Identity Structure
Every VerusID is an on-chain object returned by getidentity. Here's what it contains:
// getidentity "veruscx@" { "name": "veruscx", "identityaddress": "i6QEbNsGThm2JHUwpLwxSu77DwzFWvfHv6", "primaryaddresses": ["RAhD9vgrWZ3ALkq541xdqL2oLYEvsLNrzn"], "minimumsignatures": 1, "revocationauthority": "i6QEb...", // can revoke access "recoveryauthority": "i6QEb...", // can recover identity "flags": 0, // 0=unlocked, 2=locked "timelock": 0, // unlock delay (blocks) "contentmultimap": {}, // on-chain data store "contentmap": {} }
| Field | Purpose |
|---|---|
| name | Human-readable identity name (unique on-chain) |
| primaryaddresses | Addresses that can spend/sign for this identity |
| minimumsignatures | Multi-sig threshold (1 = single sig) |
| revocationauthority | Identity that can revoke (lock) this ID. Can be self or another ID |
| recoveryauthority | Identity that can recover (update keys). Can be self or another ID |
| flags | Bit 1 = locked. When locked, no transactions can be sent |
| timelock | Minimum block height before unlocking takes effect |
| contentmultimap | Key-value data store (VDXF keys to arrays of values) |
Content Multimap
Every VerusID has a built-in on-chain data store called the contentmultimap. It maps VDXF keys to arrays of values — each value up to ~4KB. Think of it as a blockchain-native key-value database attached to your identity.
Writing data
Data is written via updateidentity. Values are typically hex-encoded JSON stored under a VDXF key.
# Store data in your identity DATA=$(echo -n '{"type":"post","title":"Hello"}' | xxd -p | tr -d '\n') ./verus updateidentity '{ "name": "yourname", "parent": "iParentIdHere", "contentmultimap": { "iVDXFKeyHere": ["'$DATA'"] } }'
Reading data
Read via getidentity and decode the hex values. From a browser, call the public RPC:
const resp = await fetch('https://api.verus.services', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '1.0', id: '1', method: 'getidentity', params: ['veruscx@'] }) }); const { result } = await resp.json(); const cmm = result.identity.contentmultimap; // Decode hex values: hex → bytes → UTF-8 → JSON
Encryption
Data can be encrypted using Verus's built-in Sapling (zk-SNARK) addresses. Encrypt with signdata using an encrypttoaddress parameter, then store the encrypted descriptor in the contentmultimap. Decrypt with decryptdata — only the holder of the z-address private key can read it.
This enables selective disclosure: encrypt each field separately, share viewing keys per-field to reveal only what's needed.
Vault Security
Every VerusID has built-in security features that don't exist in normal crypto wallets.
Lock & Revoke
Lock your identity so no transactions can be sent — even if your private key is stolen. Only the revocation authority can unlock, and only after a timelock delay.
# Lock an identity ./verus updateidentity '{"name":"veruscx","flags":2}' # Unlock (starts timelock countdown) ./verus updateidentity '{"name":"veruscx","flags":0}'
Recovery
Lost your key? Your recovery authority can update the identity to use a new primary address. Set it to yourself (self-sovereign) or a trusted party like your spouse, company, or friend.
This is impossible with Bitcoin, Ethereum, or any standard crypto wallet. With VerusID, losing your key doesn't mean losing your funds.
Timelock
Set a minimum block height before an unlock takes effect. Even if an attacker has your revocation key, they can't move funds instantly — you have time to notice and respond.
Key RPC Methods
| Method | Purpose |
|---|---|
getidentity | Look up any identity by name or i-address |
updateidentity | Update primary addresses, flags, contentmultimap, authorities |
registernamecommitment | First step to register a new VerusID |
registeridentity | Complete VerusID registration |
signdata | Sign or encrypt data with an identity (supports MMR, z-addr encryption) |
decryptdata | Decrypt data encrypted to a z-address |
verifydata | Verify a signature from an identity |
signmessage | Sign a message with an address (used for challenges) |
verifymessage | Verify a signed message |
Reading Identity Data
getidentity — confirmed vs mempool
The height parameter controls what state you see. Use -1 to include unconfirmed (mempool) identity updates.
// Confirmed state only (default) getidentity "alice@" // Include mempool — see updates before they confirm (~1 min faster) getidentity "alice@" -1 // State at a specific block height (historical) getidentity "alice@" 4000000
Use -1 when you need fast detection — e.g. checking if a user just wrote subscription metadata to their identity. The data appears in seconds instead of waiting for a block.
Used in: Subscriptions, KYC, Blog
getidentityhistory — full change log
Returns every version of an identity across a block range. Useful for tracking when contentmultimap data was added or changed.
// All changes from block 0 to current tip getidentityhistory "alice@" 0 -1
Returns an array of identity states, each with a height and the full identity object at that point. Useful for audit trails and version history.
Used in: KYC (audit trail), Subscriptions (detecting new subscribers)
Reading contentmultimap
Hex decoding
Values in the contentmultimap are hex-encoded. Decode to UTF-8, then parse as JSON.
// JavaScript const id = await rpc("getidentity", ["alice@"]); const cmm = id.identity.contentmultimap; // Decode a hex value const hex = cmm["iVDXFKeyHere"][0]; const json = JSON.parse(Buffer.from(hex, "hex").toString("utf8"));
Used in: Blog, Social, Subscriptions
VDXF keys
Keys in the contentmultimap are VDXF i-addresses — deterministic identifiers generated from qualified names. Same name always produces the same key on every node.
// Generate a VDXF key getvdxfid "vrsc::identity.blog.post" // Returns: { "vdxfid": "iGTUp2Tnd2eYAih717dyFHzbokhy6DgrH2", ... } // Standard keys (same on every node) vrsc::identity.firstname → iLB8SG7ErJtTYcG1f4w9RLuMJPpAsjFkiL vrsc::identity.email → iJ4pq4DCymfbu8SAuXyNhasLeSHFNKPr23 vrsc::identity.blog.post → iGTUp2Tnd2eYAih717dyFHzbokhy6DgrH2
Writing contentmultimap
Read-merge-write pattern
updateidentity replaces the entire contentmultimap. Always read the current state first, merge your changes, then write back.
// 1. Read current identity const id = await rpc("getidentity", ["alice@"]); const existing = id.identity.contentmultimap || {}; // 2. Add your new data (preserve existing keys) const newHex = Buffer.from(JSON.stringify(myData)).toString("hex"); existing["iMyVDXFKey"] = [newHex]; // 3. Write back the full contentmultimap await rpc("updateidentity", [{ name: "alice", parent: "iParentId", contentmultimap: existing }]);
Gotchas
updateidentityfails if another update for the same identity is unconfirmed — wait for prior updates to confirm- ~5500 byte limit per
updateidentitytransaction — keep payloads compact, store large data off-chain - Use
name+parentfields, neverfullyqualifiedname - Always verify
revocationauthorityandrecoveryauthorityare not set to SELF before revoking
Encryption & Selective Disclosure
signdata — encrypt per-field
Encrypt data to a Sapling z-address. Each field gets its own ephemeral key, enabling per-field selective disclosure.
// Encrypt and sign data const result = await rpc("signdata", [{ address: "alice@", createmmr: true, encrypttoaddress: "zs1your_z_address_here...", mmrdata: [{ vdxfdata: { "iDDKeyHere": { version: 1, label: "iFieldVDXFKey", mimetype: "text/plain", objectdata: { message: "secret data" } } } }] }]); // result.mmrdescriptor_encrypted → store this on-chain // result.signaturedata → attestation signature
Used in: KYC (per-field encryption), Social (tiered post encryption)
decryptdata — selective disclosure
Decrypt individual fields from an encrypted MMR descriptor. Each field has its own ephemeral public key (EPK) — share the EPK for just the fields you want to disclose.
// Decrypt a single field from the encrypted descriptor const decrypted = await rpc("decryptdata", [{ datadescriptor: descriptor.datadescriptors[0] }]); // Only the z-address holder can decrypt
Viewing keys
Share a viewing key to let someone decrypt all data encrypted to a z-address — without giving them spending rights.
// Export the viewing key for a z-address z_exportviewingkey "zs1your_z_address..." // Give this key to a verifier — they can decrypt but not spend
For tiered access (public/followers/close friends), use a different z-address per tier. Share the viewing key only with people in that tier. Rotate the z-address to revoke access.
Used in: Social (4-tier access control), KYC (viewing key disclosure)
Chain Queries
getaddressdeltas — transaction history for an address
Returns all balance changes for an address. Positive satoshis = received, negative = spent. Useful for verifying payments without trusting a local database.
// All transactions for an address getaddressdeltas { "addresses": ["RAddressHere"] } // Filter by block range getaddressdeltas { "addresses": ["RAddr"], "start": 4000000, "end": 4001000 } // Each delta: { txid, index, address, satoshis, height, blocktime } // Negative satoshis = spent from this address
Verifying payments on-chain
Instead of maintaining a local database of who paid, query the chain directly. Count outgoing transactions from a dedicated address to the recipient.
// Get all outgoing txids from a dedicated address const deltas = await rpc("getaddressdeltas", [{ addresses: [dedicatedAddr] }]); const outTxids = [...new Set(deltas.filter(d => d.satoshis < 0).map(d => d.txid))]; // Verify each TX pays the right destination for (const txid of outTxids) { const tx = await rpc("getrawtransaction", [txid, 1]); const paysProvider = tx.vout.some(v => v.scriptPubKey?.addresses?.includes(providerIAddr) ); }
This is how the subscription demo verifies payments — purely chain-based, no local state. The server stores nothing about the subscriber.
Used in: Subscriptions (chain-based access verification, GDPR-compliant)
Currency Operations
sendcurrency
Send native or reserve currencies. Returns an operation ID — poll z_getoperationresult for the actual txid.
// Send VRSC sendcurrency "fromAddress" '[{"address":"RDest","amount":1.0}]' // Send a reserve currency (e.g. vETH) sendcurrency "fromAddress" '[{"address":"RDest","amount":0.01,"currency":"vETH"}]' // Poll for result z_getoperationresult '["opid-abc123..."]' // Returns: [{ status: "success", result: { txid: "abc..." } }]
Warning: Never use * as the from address — always use a specific wallet address. Using wildcard can cause unexpected behaviour.
Time-locked transactions (nLockTime)
Create transactions that can't be broadcast until a specific block height. Used for scheduled payments and subscriptions.
// Create a transaction locked until block 4100000 createrawtransaction \ '[{"txid":"fundTxid","vout":0}]' \ '{"iPaymentAddr": 0.0099}' \ 4100000 // nLockTime — earliest block to broadcast \ 4200000 // expiryheight // Sign it now, broadcast later when locktime is reached signrawtransaction "rawTxHex" // The network rejects early broadcasts — consensus enforced
createrawtransaction requires i-addresses, not friendly names. Resolve with getidentity first.
Used in: Subscriptions (pre-signed recurring payments)
Oracle (REST)
USD pricing for every Verus currency, derived from on-chain basket reserves. Hosted at scan.verus.cx. JSON, no auth.
GET /api/oracle/prices
List of every currency the oracle currently prices (filtered to currencies with at least $10k in stable backing depth).
// curl https://scan.verus.cx/api/oracle/prices [ { "currencyId": "i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV", "currencyName": "VRSC", "usdPrice": 0.7646, "emaPrice": 0.7649, "source": "reserve", "sourceBlock": 4032417, "sourceBasket": "Pure", "confidence": 50, "status": "healthy", "totalDepth": 9426614.83, "updatedAt": "2026-04-20T07:52:46.835Z" } ]
GET /api/oracle/prices/:id
Detailed view for one currency: current price, every basket sourcing it, guard-rail check against external feeds (CoinGecko / Binance), and recent price history.
:id accepts any of: i-address (i5w5MuNik5...), name (VRSC, case-insensitive), or fully-qualified name (tBTC.vETH). Bare DAI resolves to DAI.vETH.
// curl https://scan.verus.cx/api/oracle/prices/i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV { "currencyId": "i5w5Mu...", "currencyName": "VRSC", "currentPrice": { "usdPrice": 0.7646, "sourceBasket": "Pure", ... }, "liveCalculation": { "weightedPrice": ..., "baskets": [ ... ] }, "guardRail": { "externalPrice": ..., "deviationPct": ... }, "priceHistory": [ ... last 50 snapshots ... ] }
GET /api/oracle/prices/:id/history
Time-series price snapshots for charting. Same :id resolution as above (i-address, name, or fully-qualified). Query params: from=<block>, to=<block>, limit=<n> (default 200, max 1000).
// curl 'https://scan.verus.cx/api/oracle/prices/i5w5Mu.../history?limit=5' { "currencyId": "i5w5Mu...", "count": 5, "data": [ { "usdPrice": 0.7731, "blockHeight": 4032417, "basket": "Pure", "confidence": 50, "createdAt": "2026-04-20T07:52:46.835Z" } ] }
GET /api/oracle/conversions/:txid
USD value at execution time for a given conversion (one row per vout). Pricing rule: stablecoin side first if present, otherwise amount × oracle_price of the to-currency. Returns null for the rare basket-to-basket case.
// curl https://scan.verus.cx/api/oracle/conversions/<txid> { "txid": "7e93a938...", "count": 1, "data": [{ "chainId": "vdex", "fromCurrencyId": "iGBs4DWz... (DAI)", "toCurrencyId": "i9nwxtKu... (vETH)", "amountIn": 10, "amountOut": 0.00440481, "usdValue": 10, "rule": "stable_in", "pricedCurrencyId": "iGBs4DWz... (DAI)", "pricedCurrencyUsd": 1 }] }
Possible rule values:
stable_in | From-side is DAI/vUSDT/vUSDC — value = amount_in |
stable_out | To-side is DAI/vUSDT/vUSDC — value = amount_out |
to_basket_from | To-side is a basket — priced via the from-side's USD price |
to_side | Both crypto, neither basket — priced via the to-side's USD price |
unpriced | Rare basket-to-basket case; usdValue is null |
Libraries
verus-typescript-primitives — Core types: LoginConsentRequest, VerusPayInvoice, TransferDestination, VDXF keys.
verusid-ts-client — VerusID interface for signing, verifying, and RPC interaction.
verus-connect — Drop-in VerusID login and payments for any website.