VerusID authentication, payments, and identity requests for any website. Add a "Sign in with VerusID" button to your site, and the wallet's consent prompt will show your app's identity (your own VerusID) — the user is approving the relationship with your app, not with a third party.
- What you get
- What you provide
- Try it locally in 5 minutes
- Three integration axes (pick one per axis)
- Why each app needs its own signer
- Quick Start — three paths
- Frontend integration
- Configuration reference
- API endpoints
- Login flow
- Evidence schema & trust model
- Multi-chain support
- Web component reference
- CORS
- Operating in production
- Other features
- Production integrations
- Dependencies
- Security model summary
- License
- Disclaimer
A 4-step user flow:
1. User clicks "Sign in with VerusID" on your site
2. Your page shows a QR code (desktop) or a deeplink button (mobile)
3. The wallet shows "<your-app-id>@ wants to verify you"
User taps approve
4. Your page receives a `verified` event with the user's iAddress + a
cryptographic `evidence` receipt. Mint a session and you're done.
The receipt (evidence) lets your backend (or any third party) re-prove the login against any Verus RPC node — you don't have to trust this sidecar.
- Your own VerusID on the chain(s) you'll authenticate against. This is the app's identity, not the user's. The wallet shows it in the consent prompt.
- A public URL the user's wallet can reach (your production domain; ngrok/cloudflared for local testing).
- Either a local verusd OR a WIF private key for your identity (one or the other — see Axis A below).
That's it. No accounts to register, no API keys, no monthly fee. Verus is decentralised — verus-connect just speaks the protocol.
git clone https://github.com/Fried333/verus-connect.git
cd verus-connect
npm install && npm run build
cd examples/signin-lite
npm install
cp .env.example .env
chmod 600 .env
$EDITOR .env # fill SIGNING_IADDRESS, PRIVATE_KEY, CALLBACK_URL
node server.js
# → http://localhost:3030You'll need a VerusID on VRSC (with verus dumpprivkey "yourid@" for the WIF) and a callback URL the wallet can reach (ngrok / cloudflared / your LAN IP — localhost won't work from a phone). See examples/signin-lite/README.md for the full walkthrough.
verus-connect lets you make three independent choices. Decide one per axis and combine.
| Lite mode | Daemon mode | |
|---|---|---|
| Caller runs | this npm package only | this npm package + a local verusd |
| Caller signs as | their own VerusID + WIF private key | their own VerusID (verusd holds the key) |
| Verifies with | a public Verus RPC node (e.g. https://rpc.verus.cx/) |
the local verusd |
| Best for | Indie apps that don't want to operate verusd | Exchanges, custodians, high-stakes, anyone already running verusd |
| Multi-chain | yes — one public RPC URL per chain | yes — local verusd per chain |
| Need WIF private key? | yes — verus dumpprivkey "yourid@" |
no — verusd holds it |
| Sidecar | SDK (Express middleware) | |
|---|---|---|
| Shape | verus-connect is a separate Node process behind nginx, listens on 127.0.0.1:PORT. Your frontend (any tech stack) calls it via HTTP. |
verus-connect is import-ed into your own Node app. Your Express server mounts the auth routes alongside your business logic. |
| Processes | 2 (your app + sidecar) | 1 |
| Your app's language | anything (Python, Go, Rust, plain HTML) — only HTTP | must be Node |
| Restart verus-connect without touching your app | yes | no — kills your whole app |
| Crash isolation | sidecar bug ≠ your app crash | bug in verus-connect can kill your app |
| Ops complexity | higher (systemd unit + nginx route) | lower (one process) |
| Templates | deploy/ (systemd + nginx + env) |
examples/signin-lite/server.js |
<verus-connect-login> web component |
Bespoke frontend | |
|---|---|---|
| What you write | One HTML tag + one event listener | Your own QR + polling JS (~50 lines) |
| Styling | CSS custom properties on the host element | full control |
| Use when | drop-in works; you want the standard UX | you have a strict design system or non-HTML UI (native mobile, etc.) |
| Reference | Web component reference below | Login flow + raw /login / /result/:id calls |
| Your situation | Axis A | Axis B | Axis C |
|---|---|---|---|
| Indie dev, static HTML site, single chain | Lite | Sidecar | Web component |
| Indie dev evaluating before committing | Lite | SDK | Web component (in examples/signin-lite) |
| Existing Node/Express app, wants minimum new infra | Lite or Daemon | SDK | either |
| Production: SvelteKit/Next/etc. frontend, custom design system | Daemon | Sidecar | Bespoke |
| Exchange or custodian, multi-chain, high-stakes | Daemon | Sidecar | Bespoke |
| Multiple sites on one box, one VerusID per site | Daemon | Sidecar | either |
Terminology note. Throughout this README, "sidecar" means verus-connect running as a separate Node process behind nginx. "SDK" or "middleware" means verus-connect
import-ed into your own Node app. Same code, different shape.
The wallet's consent prompt shows the signing identity — that's the app's identity from the user's perspective. Sharing one signer across many apps collapses the app-identity boundary the wallet relies on (it's the namespace key for per-app encrypted credentials, VDXF writes, and future DREAM-style features). Every integration should own its signer.
Pick the path that matches your Axis B choice (sidecar vs SDK) and your evaluation stage.
- Node.js 20+
- Git
- Your own VerusID on the chain(s) you'll authenticate against
- A callback URL the wallet can reach (production domain, or ngrok/cloudflared for dev)
The fastest way to see verus-connect work end-to-end. Spins up a tiny Express server bundling verus-connect as middleware. ~5 minutes.
git clone https://github.com/Fried333/verus-connect.git
cd verus-connect
npm install && npm run build
cd examples/signin-lite # or examples/signin-daemon
npm install
cp .env.example .env
chmod 600 .env
$EDITOR .env
node server.js
# → http://localhost:3030See examples/signin-lite/README.md (lite mode) or examples/signin-daemon/README.md (daemon mode) for full details.
What every real deployment looks like. The sidecar runs as a systemd service on 127.0.0.1:PORT; nginx terminates TLS and reverse-proxies the /verus/* path.
# On your production host:
git clone https://github.com/Fried333/verus-connect.git /opt/verus-connect
cd /opt/verus-connect
npm install && npm run build
# Config (daemon mode shown; swap to env.lite-multi.example for lite)
mkdir -p /etc/verus-connect
cp deploy/env.daemon.example /etc/verus-connect/yourapp.env
chmod 600 /etc/verus-connect/yourapp.env
$EDITOR /etc/verus-connect/yourapp.env
# Systemd unit
cp deploy/verus-connect-daemon.service /etc/systemd/system/yourapp-sidecar.service
$EDITOR /etc/systemd/system/yourapp-sidecar.service # set EnvironmentFile= path
systemctl daemon-reload
systemctl enable --now yourapp-sidecar.service
# nginx (reverse-proxy /verus/ to 127.0.0.1:PORT)
cp deploy/nginx-site.conf /etc/nginx/sites-available/yourapp.conf
$EDITOR /etc/nginx/sites-available/yourapp.conf # set server_name + cert paths
ln -s /etc/nginx/sites-available/yourapp.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginxThe deploy/ templates include NoNewPrivileges=true, PrivateTmp=true, ProtectSystem=strict and the matching nginx server block. See deploy/README.md for the full playbook.
npm install Fried333/verus-connect # adds verus-connect to your package.jsonimport { verusAuth } from 'verus-connect/server';
import express from 'express';
const app = express();
// Daemon mode — multi-chain (verusd holds the key)
app.use('/verus', verusAuth({
iAddress: 'youridentity@',
callbackUrl: 'https://yoursite.com/verus/verusidlogin',
chains: ['vrsc', 'varrr'],
defaultChain: 'vrsc',
}));
// — or — Lite mode (WIF in your app's memory)
app.use('/verus', verusAuth({
iAddress: 'youridentity@',
callbackUrl: 'https://yoursite.com/verus/verusidlogin',
privateKey: process.env.PRIVATE_KEY,
verifyNodeUrl: 'https://rpc.verus.cx/',
}));
app.listen(8100);Mode auto-detection: if you pass privateKey, mode is lite; otherwise daemon. Both modes accept chains[] for multi-chain operation.
Lite mode install caveat. The verusid-ts-client peer dep used by lite mode has a post-install build step that npm sometimes drops. If import { VerusIdInterface } from 'verusid-ts-client' fails at runtime, reinstall it with yarn:
yarn add verusid-ts-client@github:VerusCoin/verusid-ts-client#9b5a6f0db30ccf2a57c7a952d0191e5da7f3f195The browser side. Independent of Axis A (lite/daemon) and Axis B (sidecar/SDK) — both backends expose the same HTTP API, so the same frontend code works for both.
One HTML tag, one event listener. Works in plain HTML, React, Svelte, Vue, anything that speaks DOM.
<script type="module" src="https://your.cdn/dist/web.global.js"></script>
<verus-connect-login base="/verus"></verus-connect-login>
<script>
document.querySelector('verus-connect-login').addEventListener('verified', (e) => {
// e.detail = { iAddress, friendlyName, systemId, chainName, evidence }
fetch('/auth/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(e.detail),
});
});
</script>The element handles every API call (GET /chains, POST /login, polling GET /result/:challengeId) and regenerates the deeplink + QR whenever the user picks a different chain.
See Web component reference below for attributes, events, methods, and theming.
If you have a strict design system or need a non-HTML UI (React Native, Tauri, …), call the API directly:
// 1. List the chains the sidecar supports
const { default: defaultChain, chains } = await (await fetch('/verus/chains')).json();
renderPicker(chains, defaultChain);
// 2. When the user picks a chain (or on first render), issue a challenge
async function pickChain(chain) {
const r = await (await fetch('/verus/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chain }),
})).json();
const { challengeId, deepLinkPost, deepLinkRedirect } = r;
renderQr(deepLinkPost); // QR code (TYPE_POST envelope)
renderButton('Open in Verus wallet',
{ href: deepLinkRedirect }); // mobile button (TYPE_REDIRECT envelope)
startPolling(challengeId);
}
function startPolling(challengeId) {
const t = setInterval(async () => {
const r = await (await fetch(`/verus/result/${challengeId}`)).json();
if (r.status === 'verified') { clearInterval(t); onLoggedIn(r); }
else if (r.status === 'expired') { clearInterval(t); /* restart */ }
}, 1500);
}
// 3. Handle the mobile redirect-return path
// (when the wallet returns the user to your page after signing on the same device)
const url = new URL(location.href);
const cid = url.searchParams.get('challengeId');
const payload = url.searchParams.get('i9JzVt59mAVHqjc8WAQJx7bEFAQ4ffuhrC');
if (cid && payload) {
const bytes = base64UrlToBytes(payload); // helper: atob with -/_ → +/
await fetch(`/verus/verusidlogin/${cid}`, {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: bytes,
});
startPolling(cid);
// Strip URL params so refresh doesn't re-post.
url.searchParams.delete('challengeId');
url.searchParams.delete('i9JzVt59mAVHqjc8WAQJx7bEFAQ4ffuhrC');
history.replaceState(null, '', url.toString());
}Why two deeplinks (deepLinkPost + deepLinkRedirect)? Mobile wallets distinguish QR-scan (POST result back to your server) from same-device deeplink-click (redirect user back to your page with the response in URL params). Two envelopes prevent the wallet from ambiguously picking. The legacy deepLink field also still returns for older clients — equals deepLinkPost.
Each call to /login returns a new challengeId. Switching chains MUST regenerate — the deep link is per-chain (different systemId baked in).
Env vars (sidecar mode reads them from the env file; SDK mode reads them via process.env or you pass them inline to verusAuth()).
| Variable | Required | Mode | Description |
|---|---|---|---|
SIGNING_IADDRESS |
Yes | Both | VerusID to sign challenges with. Must exist on every chain in CHAINS. |
CALLBACK_URL |
Yes | Both | URL the wallet POSTs signed responses to (QR-scan path). Sidecar appends /<challengeId> per challenge. Must end with /verus/verusidlogin (or whatever path nginx routes to the sidecar). |
REDIRECT_URL |
No | Both | URL the wallet redirects users to after sign (deeplink-click path). Defaults to the origin of CALLBACK_URL. Sidecar appends ?challengeId=<id>; wallet appends &i9JzVt59...=<base64url-response>. Set explicitly if you want a different landing page than /. |
CHAINS |
Yes | Both | Comma-separated chain names (e.g. vrsc,varrr,vdex,chips). Daemon mode: each chain needs a local verusd. Lite mode: each chain needs a VERIFY_NODE_URL_<CHAIN>=. |
DEFAULT_CHAIN |
Yes | Both | Chain used for challenge issuance when client omits it. Must be in CHAINS. |
CONF_PATH_<NAME> |
No | Daemon | Override a chain's conf file path (uppercase chain name). Default lookup table below. |
PRIVATE_KEY |
Yes | Lite | WIF private key for offline signing. Must derive SIGNING_IADDRESS's current primary R-address — if rotated, update this. |
VERIFY_NODE_URL |
single-chain lite | Lite | Public node URL for verification when only one chain is used. Either this or VERIFY_NODE_URL_<CHAIN> is required. |
VERIFY_NODE_URL_<CHAIN> |
multi-chain lite | Lite | Per-chain public node URL. One entry required for every name in CHAINS. e.g. VERIFY_NODE_URL_VARRR=https://varrr.rpc.verus.cx/. |
CHAIN |
No | Lite | Legacy single-chain selector. Ignored when CHAINS is set. Default: vrsc. |
API_URL |
No | Both | Public API for identity resolution (default: https://api.verus.services). |
PORT |
No | Both | Server port (default: 8100). |
HOST |
No | Both | Server host (default: 127.0.0.1). |
CORS_ORIGINS |
No | Both | Allowed origins, comma-separated. Default: locked to CALLBACK_URL's origin. See CORS before opening up. |
DEBUG |
No | Both | 1 or true to enable verbose logging. |
| Chain | Conf path |
|---|---|
| VRSC | ~/.komodo/VRSC/VRSC.conf |
| PBaaS (vARRR, vDEX, CHIPS, …) | ~/.verus/pbaas/<hex>/<hex>.conf where <hex> is the chain system_id's hash160 in reversed byte order |
Override per-chain if your conf lives somewhere non-standard:
CONF_PATH_VRSC=/custom/path/VRSC.conf| Name | Display | system_id |
|---|---|---|
vrsc |
VRSC | i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV |
varrr |
vARRR | iExBJfZYK7KREDpuhj6PzZBzqMAKaFg7d2 |
vdex |
vDEX | iHog9UCTrn95qpUBFCZ7kKz7qWdMA8MQ6N |
chips |
CHIPS | iJ3WZocnjG9ufv7GKUA4LijQno5gTMb7tP |
To add a new chain, edit KNOWN_CHAINS in src/chain-registry.ts.
| Method | Path | Description |
|---|---|---|
GET |
/chains |
List supported chains + live health → { default, chains: [{ name, displayName, systemId, healthy, lastChecked }] } |
POST |
/login |
Create a login challenge. Body: { chain?: string } (defaults to DEFAULT_CHAIN) → { challengeId, deepLink, deepLinkPost, deepLinkRedirect, chain, chainName, systemId } |
POST |
/verusidlogin/:id |
Receive signed response from wallet. Body: raw GenericResponse bytes (Content-Type: application/octet-stream) → { status: "ok" } |
GET |
/result/:challengeId |
Poll for result → { status: "pending" } or { status: "verified", iAddress, friendlyName, systemId, chainName, evidence } or { status: "expired" }. See Evidence schema. |
| Method | Path | Description |
|---|---|---|
POST |
/pay-deeplink |
Generate a VerusPay invoice deep link → { deep_link, resolved_address } |
POST |
/generic-request |
Create a GenericRequest deep link → { deep_link, qr_string } |
POST |
/identity-update-request |
Create an identity update request → { deep_link, qr_string } |
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check → { status: "ok", mode, primitivesLoaded } |
1. Browser GET /chains
← { default: "vrsc", chains: [{name:"vrsc",healthy:true}, {name:"varrr",healthy:true}] }
Render picker, grey out unhealthy chains
2. Browser POST /login { chain: "varrr" }
← { challengeId, deepLinkPost, deepLinkRedirect, chain:"varrr", chainName:"vARRR", systemId:"iExB..." }
3. Show QR / button QR encodes deepLinkPost; mobile button uses deepLinkRedirect
Browser starts polling GET /result/:challengeId every 1.5s
4. User signs (a) QR-scan path: wallet POSTs to CALLBACK_URL/<challengeId>
directly → sidecar verifies, /result transitions to verified
(b) Deeplink-click path: wallet redirects user back to
REDIRECT_URL?challengeId=<id>&i9JzVt59...=<base64url-response>
→ browser POSTs the response bytes to /verusidlogin/<id>
→ sidecar verifies, /result transitions to verified
5. Browser polls GET /result/:challengeId
← { status: "verified", iAddress, friendlyName, systemId, chainName, evidence }
6. Done Browser fires `verified` event → your code mints a session.
Persist systemId so you know which chain authenticated.
The sidecar rejects logins for chains not in CHAINS with an explicit error — there's no silent fallback. Verification is routed to the daemon (or RPC node) whose system_id matches the response's signature.systemID.
Every verified /result includes an evidence object — the cryptographic receipt the relying party can re-verify against any Verus node without trusting this server:
interface LoginEvidence {
decisionHash: string // hex SHA256 of the wallet's signed decision
decisionSignature: string // base64 signature by the user's identity
challengeHash: string // hex SHA256 of the server-issued challenge
challengeSignature: string // base64 signature by the server's signing identity
challengeSigningId: string // i-address of the server's signing identity
systemId: string // canonical chain i-address
verifiedAt: number // Unix seconds when verification completed
}Independent re-verification — your backend (or any third party) calls a Verus node's verifysignature RPC:
curl -X POST https://api.your-trusted-node.example -H "Content-Type: text/plain" -d '{
"jsonrpc": "1.0", "method": "verifysignature",
"params": [{
"address": "<result.iAddress>",
"signature": "<result.evidence.decisionSignature>",
"datahash": "<result.evidence.decisionHash>"
}]
}'
# → { "result": { "signaturestatus": "verified", "identity": "alice@",
# "system": "VRSC", "signatureheight": 4067136, ... } }The daemon response reveals signatureheight (block when the signature was minted) — your code can reject signatures more than N blocks old for stronger replay protection.
| Actor | What they can do | What they can't do |
|---|---|---|
| Malicious wallet | Refuse to sign | Forge a signature (verified against the user's on-chain identity) |
| Malicious user (no wallet access) | Submit garbage to /verusidlogin/:id |
Bypass signature verification — the daemon's verifysignature checks against the on-chain primary R-address |
| Malicious third-party site | Try to use your sidecar as a shared IdP | Be blocked by CORS lock to CALLBACK_URL origin (default) |
| Malicious frontend (someone with write access to your HTML) | Show a fake "Sign in" UI | Skip signature verification — evidence is checked server-side against the on-chain identity |
| Malicious sidecar operator | Refuse to issue challenges; lie about results to first-party calls | Forge a verified evidence block — it would fail re-verification at any Verus node |
| Network attacker | MITM if you don't use HTTPS | Forge a signed envelope (signatures don't depend on transport) |
The sidecar is not the trust anchor — the Verus chain is. Even if you completely don't trust the sidecar, re-verifying the evidence block against any RPC node gives you the same security guarantees.
verus-connect supports authenticating users on any combination of VRSC + PBaaS chains on a single sidecar. The user picks which chain in the QR/button UI; the sidecar routes verification to the right daemon (or RPC) automatically.
SIGNING_IADDRESS must exist on every chain in CHAINS. Run this once per chain after creating the identity on VRSC:
# fund the identity once (skip if its i-addr already has a UTXO)
verus sendtoaddress "yourid@" 0.01
# export to vARRR
verus sendcurrency "yourid@" '[{"address":"yourid@","exportto":"vARRR","exportid":"true","amount":0}]'The amount:0 form is required — anything else triggers consensus checks for converters that aren't needed. Source must be the identity itself (R-address holding the primary key fails the controller check). ~0.002 VRSC fee per export.
Repeat for each PBaaS chain (vDEX, CHIPS, etc.).
⚠️ Cross-chain identity export is permanent. All controllers (revoke/recovery authorities) must consent. The exported identity is created on the destination chain with no control token, and this state is permanent — once exported, the identity on that chain can never be linked back to a control token. Plan accordingly.
| Attribute | Default | What it does |
|---|---|---|
base |
/verus |
URL prefix where the sidecar is mounted. Matches the nginx location block. |
default-chain |
server's DEFAULT_CHAIN |
Chain selected on first render. The picker still shows every chain in CHAINS. |
poll-interval-ms |
1500 |
How often to poll /result/:id. |
| Event | detail |
|---|---|
verified |
{ iAddress, friendlyName, systemId, chainName, evidence } |
error |
{ message } — network or sidecar error |
expired |
— challenge ran past its 5-minute window without a wallet response |
const el = document.querySelector('verus-connect-login');
el.regenerate(); // force a new challenge for the current chainOverride these CSS custom properties on the host element:
verus-connect-login {
--vc-primary: #4f46e5; /* selected pill + deeplink button */
--vc-primary-fg: #fff;
--vc-fg: #111; /* default text */
--vc-fg-muted: #666; /* status line */
--vc-pill-bg: #eef;
--vc-qr-bg: #f7f7fa;
}The element uses an open shadow root — styles inside don't leak out, the host element is the only restyling surface.
The published package exposes the bundle at verus-connect/web for npm consumers, plus a standalone dist/web.global.js for plain <script> drops. Both have qrcode bundled inline — no peer install required.
// bundler path
import 'verus-connect/web'; // side-effect import; registers the element
// vanilla path
// <script type="module" src=".../verus-connect/dist/web.global.js"></script>Cache-bust the bundle URL with a ?v=X.Y.Z query string when you depend on a newly-released SDK behaviour — the bundle URL itself is stable, so browsers will happily serve a stale copy from cache for hours.
verus-connect ships private by default: endpoints are mounted but CORS is locked to CALLBACK_URL's origin, so only requests from your own site can reach them. That's the right default — your sidecar authenticates users of your app, not third-party callers under your signer.
Don't open this up to act as a shared identity provider for other people's apps. Doing so means strangers' sites trigger wallet prompts that say <yourapp.com> wants to verify you, conflating their identity with yours — and collapses the per-app signer boundary the wallet relies on.
If you genuinely need cross-origin access (e.g. browser on one host, sidecar on another during dev, or you operate multiple first-party properties under the same signer), set an explicit allow-list:
CORS_ORIGINS=https://yourapp.com,https://app.yourapp.comSetting CORS_ORIGINS=* is supported but logs a startup warning — prefer an explicit list.
Sidecar mode (systemd):
journalctl -u yourapp-sidecar.service -f # follow live
journalctl -u yourapp-sidecar.service --since "1h ago" # last hourSDK mode: logs go wherever your Node app's stdout/stderr go.
Set DEBUG=1 in the env file for verbose logging (includes per-request input shapes — leave OFF in steady-state production).
| Log message | Means | Fix |
|---|---|---|
Challenge creation failed: This wallet does not contain valid signing keys for <id> |
(daemon mode) verusd doesn't have the primary R-address WIF for SIGNING_IADDRESS |
verus importprivkey "<wif>" "" false then restart sidecar |
Webhook error: Cannot read slice out of bounds |
Wallet POSTed an envelope shape the sidecar can't parse | Usually a wallet/SDK version skew — upgrade the sidecar (git pull && npm install && npm run build && systemctl restart) |
Verification error (500 on /verusidlogin/:id) |
Response signature didn't verify — wrong identity, rotated key, or wrong chain | Check identity primary-address history; confirm the wallet signed as the expected identity |
Challenge not found or expired (404 on /verusidlogin/:id) |
Wallet POSTed for a challengeId older than 5 min, or never issued |
User should retry — fresh challenge needed |
Signing identity is not active |
The user's identity has been revoked on-chain | Per Verus protocol — revoked identities can't sign. Nothing to fix on the sidecar |
<chain> is temporarily unavailable |
Health check found the daemon/RPC unreachable | Check verus -chain=<X> getinfo; if RPC node down, /login will reject until it recovers |
chain <X> not supported |
Client requested a chain not in CHAINS= |
Add it to CHAINS=, restart |
Lite mode: if you rotate SIGNING_IADDRESS's primary R-address on-chain (updateidentity), update PRIVATE_KEY to the new WIF and restart. The old WIF won't sign as the rotated identity any more.
Daemon mode: if the primary R-address rotates, import the new WIF into verusd's wallet:
verus importprivkey "<new-wif>" "" false # "" = no label, false = no rescan
systemctl restart yourapp-sidecar.service # so the sidecar re-reads identity statecd /opt/verus-connect
git pull
npm install # picks up any new pinned deps
npm run build
systemctl restart yourapp-sidecar.serviceNo DB schema, no migrations — sidecar state is in-memory only (5-min challenge TTL). Brief auth-flake window during restart (existing challenges are dropped); new logins work as soon as the process is up (~1 sec).
The sidecar is stateless — challenges live in memory and auto-expire after 5 min. Nothing to back up beyond:
- The env file (
/etc/verus-connect/yourapp.env) — containsPRIVATE_KEYin lite mode, just config in daemon mode - (Daemon mode) the verusd wallet.dat, which holds the actual signing key
- The systemd unit + nginx config (in
deploy/if you used the templates)
Treat the lite-mode .env like any other secret: file mode 600, root-owned, never commit to git.
verus-connect also exposes the Verus protocol's payment-request and identity-update flows. Same envelope shape as auth.
1. Your app POST /pay-deeplink
{ address: "alice@", amount: 1.5 }
← { deep_link: "vrsc://...", resolved_address: "i..." }
2. Show QR code Encode deep_link as QR
3. User scans Verus Mobile shows payment confirmation
4. User pays Transaction broadcast on-chain
1. Your app POST /identity-update-request
{ identity: "alice@", updates: { contentmultimap: { ... } } }
← { deep_link: "vrsc://...", qr_string: "..." }
2. Show QR code User scans, wallet shows proposed changes
3. User approves Wallet signs and broadcasts updateidentity transaction
Live deployments using <verus-connect-login> as their VerusID auth surface. Useful as reference implementations — both are intentionally minimal-changes integrations to show the SDK is drop-in.
| Site | Stack | Integration shape |
|---|---|---|
| crypto-world.verus.cx | React + Vite SPA (frontend), FastAPI (backend) | Component mounted on the auth screen alongside Telegram / passphrase / MetaMask. verified event POSTs to a small /api/auth/verus/finalize backend endpoint that re-polls the sidecar's /result/:id for the verified payload and mints the site's session JWT. |
| rugpull.verus.cx | Static HTML + FastAPI | Component embedded in the existing inline-JS login card. verified event calls the existing GET /api/auth/verus/status/:id endpoint to mint the session JWT — no new backend code needed. |
| scan.verus.cx | SvelteKit | Bespoke Svelte page (not the web component) calling the sidecar's API directly; uses the manual fetch pattern documented in Frontend integration § Option B. |
The reference dev guide + interactive demo at verus.cx/dev/verus-connect/ and verus.cx/dev/demos/login/ embed the component too — those are the simplest possible integrations (<script src="…"></script><verus-connect-login base="/verus"></verus-connect-login> + an event listener that logs to a <pre>).
- Single chokepoint for
verifiedevent dispatch: the SDK constructs the event in exactly one private helper. Any field added/changed there flows to every dispatch path automatically. Don't roll your own dispatch from new code paths — call the shared helper. - Cache-bust the bundle URL with a
?v=X.Y.Zsuffix when you depend on a newly-released SDK behaviour. - Each site keeps its own visual style via CSS custom properties on the host element — no SDK fork needed.
- Re-verify server-side: the
verifiedevent detail carries the fullevidenceblock; your backend should independently verify it (viaverifysignatureagainst a Verus RPC node) rather than trusting the client.
The HTTP layer is plain Express; everything Verus-specific comes from two upstream libraries:
express— HTTP server and middleware. Used by both modes.verus-typescript-primitives— required for both modes. Builds and serialises every envelope (GenericRequest,GenericResponse, invoices, deep links). Pinned to a specific commit hash for supply-chain safety.verusid-ts-client— required for lite mode only. Off-chain signing/verification against a public Verus RPC. Loaded dynamically; daemon mode never touches it. Install withyarn run install:lite(pinned to a commit SHA).
verus-connect
├── express HTTP server
├── verus-typescript-primitives Envelope serialisation (both modes)
│ ├── bs58check Address encoding
│ ├── bn.js Big number arithmetic
│ ├── base64url Deep link encoding
│ ├── create-hash Hashing
│ └── blake2b Hashing
└── verusid-ts-client (lite only) Offline signing + RPC-based verification
2 production deps for daemon mode (express + primitives). Lite adds 1 (verusid-ts-client).
(Full per-actor trust table in Evidence schema & trust model.)
- Daemon mode: private keys never leave each daemon. Only RPC calls. RPC creds read from each chain's
.conffile and never logged. - Lite mode: WIF key held in memory only. Never logged, never returned in any response. Protect your
.env(mode 600). - Chain isolation: every verification routed to the daemon whose
system_idmatches the response. Chains not inCHAINSare rejected — no silent fallback. - Periodic health checks: each daemon pinged every 30s; unhealthy chains are flagged in
/chainsand/loginrejects challenges for them. - Rate limiting: 30 requests/minute per IP on all POST routes.
- Input validation: addresses (alphanumeric only), amounts (positive, max 1 trillion), flags (0-15), minimumsignatures (1-13), VDXF keys (i-address format).
- Error sanitisation: internal error details never reflected to clients.
- Challenge expiry: 5 minutes. Auto-cleaned every 60s.
- Body size limits: 4 KB (login), 10 KB (pay-deeplink), 100 KB (generic-request, identity-update), 1 MB (wallet callback).
- CORS: locked to
CALLBACK_URL's origin by default. See CORS. - No remote code: everything bundled at build time. Dependencies pinned to exact versions / commit SHAs.
MIT — see LICENSE if present, or the standard MIT terms.
This software is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
Run your own security review before putting this in front of real users or real money. The trust model, key handling, and verification chain are documented in Evidence schema & trust model and Security model summary — verify they match your own threat model.