Decentralized recurring payments using pre-signed time-locked transactions. No smart contracts, no intermediaries. Look up any VerusID to see its subscription terms or active subscriptions. The hex contentmultimap data is decoded to readable JSON automatically.
Subscribe to any VerusID provider in 3 steps:
Your funds are safe — Signed transactions can only pay the provider. Nobody can redirect them.
Schedule enforced by the network — nLockTime is a consensus rule. Early broadcasts are rejected.
Cancel anytime — Move your remaining funds out. All future payments become permanently invalid.
No smart contracts — Pure Bitcoin-style UTXO mechanics. No oracles, no bridges, no intermediaries.
Provider never stores your identity — The broadcast service only holds signed TXs keyed by dedicated address. Access verification is purely chain-based — the provider reads your VerusID on-chain and counts payments. Nothing about you is stored on the provider's server.
Complete guide to building decentralized subscriptions on Verus. No smart contracts needed — just VerusID contentmultimap, nLockTime, and standard RPC calls.
Register a VerusID that will act as your subscription service. This identity holds your subscription terms on-chain.
// Register a sub-ID under an existing identity (costs 0.01 VRSC) verus registernamecommitment myservice youridentity@ RYourControlAddress // Wait 1 block, then register verus registeridentity '{"txid":"commitTxid","name":"myservice","parent":"iParentId","primaryaddresses":["RYourAddress"]}' // Result: myservice.youridentity@ is now your provider identity
Store your plan details on-chain using the veruspay.vrsc::subscription.terms VDXF key. The wallet reads these terms to show the user what they're subscribing to.
// VDXF key for subscription terms i8iZrgfNEB5c8oEGENRC9B5Cv8Agvv3mqv veruspay.vrsc::subscription.terms // Terms format (hex-encode this JSON and store in contentmultimap) { "version": 1, "plans": [{ "planId": "basic", "label": "Basic Plan", "amount": 5.0, // per-period payment "currency": "VRSC", // VRSC, vETH, or any Verus currency "intervalBlocks": 43200, // ~30 days (1 block/min) "periods": 12, // number of payments "paymentAddress": "myservice.youridentity@" }] }
// Hex-encode and publish via updateidentity // First, get the current identity verus getidentity "myservice.youridentity@" // Then update with terms (spread full identity object + add contentmultimap) verus updateidentity '{ "name": "myservice", "parent": "iParentId", "primaryaddresses": ["RYourAddress"], "contentmultimap": { "i8iZrgfNEB5c8oEGENRC9B5Cv8Agvv3mqv": [ "HEX_ENCODED_TERMS_JSON_HERE" ] } }'
Important: updateidentity replaces the entire contentmultimap. Always read the existing identity first, merge your changes, then write back all keys.
When a user subscribes, the wallet performs these steps automatically. Here's what happens under the hood:
3a. Generate a dedicated address
verus getnewaddress // Returns: RDedicatedAddress123... // This address holds the subscription UTXOs. One per subscription.
3b. Fund with N UTXOs (one per payment period)
// VRSC subscription: simple outputs verus sendcurrency "fromAddress" '[ {"address":"RDedicated","amount":5.0}, {"address":"RDedicated","amount":5.0}, {"address":"RDedicated","amount":5.0} ]' // Non-VRSC (e.g. vETH): interleave currency + VRSC fee outputs verus sendcurrency "fromAddress" '[ {"address":"RDedicated","amount":0.01,"currency":"vETH"}, {"address":"RDedicated","amount":0.0001}, {"address":"RDedicated","amount":0.01,"currency":"vETH"}, {"address":"RDedicated","amount":0.0001}, {"address":"RDedicated","amount":0.01,"currency":"vETH"}, {"address":"RDedicated","amount":0.0001} ]' // Even vouts = currency, odd vouts = VRSC broadcast fee
3c. Wait for funding confirmation (non-VRSC only)
// VRSC UTXOs can be signed unconfirmed // Currency UTXOs (vETH, tokens) MUST have 1+ confirmation before signing // Poll getrawtransaction until confirmations >= 1 verus getrawtransaction "fundingTxid" 1 // Check: result.confirmations >= 1
3d. Create and sign time-locked transactions
// Resolve payment address to i-address (createrawtransaction needs i-address) verus getidentity "myservice.youridentity@" // Use: result.identity.identityaddress (e.g. "iAbc123...") // For non-VRSC, resolve currency name to i-address verus getcurrency "vETH" // Use: result.currencyid (e.g. "iGBs...") // VRSC: each TX spends 1 input verus createrawtransaction '[{"txid":"fundTxid","vout":0}]' '{"iPayAddr":4.9999}' 0 4500000 verus createrawtransaction '[{"txid":"fundTxid","vout":1}]' '{"iPayAddr":4.9999}' 4043200 4500000 // Non-VRSC: each TX spends 2 inputs (currency + fee) verus createrawtransaction '[{"txid":"fundTxid","vout":0},{"txid":"fundTxid","vout":1}]' '{"iPayAddr":{"iCurrencyId":0.01}}' 0 4500000 // Sign each transaction verus signrawtransaction "rawTxHex" // result.complete must be true
3e. Store subscription metadata on subscriber's VerusID
// VDXF key for active subscriptions iCvwWogVjiNCbiKVE38t88MEqFVFfDrjYY veruspay.vrsc::subscription.active // On-chain payload is LIGHTWEIGHT — no transactions // Signed TXs are sent directly to the broadcast service (see Step 4) // This keeps the payload ~300 bytes vs ~3500+ with embedded TXs { "version": 2, "providerId": "myservice.youridentity@", "subscriberId": "subscriber@", "dedicatedAddress": "RDedicated...", "fundingTxid": "abc123...", "startBlock": 4000000, "intervalBlocks": 43200, "totalPeriods": 12, "paymentAmount": 4.9999, "currency": "VRSC" } // ~300 bytes per subscription = room for ~15 providers on one identity // updateidentity has a ~5500 byte limit per transaction // Multiple subscriptions stored as array entries under one key // Always read-merge-write: getidentity -> filter out same provider -> append new // Wait for any prior identity update to confirm before writing
3f. Send signed transactions to the broadcast service
// The wallet returns the signed transactions to the website const result = await window.verus.subscribe({ ... }); // result.transactions = [{ period, lockTime, rawTx }, ...] // Website sends them to the broadcast service via API // IMPORTANT: No subscriber identity is sent — only the dedicated address await fetch("/api/subscription/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dedicatedAddress: result.dedicatedAddress, providerId: "myservice.youridentity@", transactions: result.transactions, startBlock: result.startBlock, intervalBlocks: result.intervalBlocks, totalPeriods: result.totalPeriods, paymentAmount: result.paymentAmount, currency: result.currency }) });
The subscription system is designed so the provider never stores subscriber identities. Two components with separate concerns:
Holds signed TXs keyed by dedicatedAddress. No identity data. Just a queue of pre-signed transactions to broadcast on schedule. Completed entries are auto-purged.
Purely chain-based. Reads the subscriber's VerusID to find their dedicatedAddress, then counts payments on-chain via getaddressdeltas. Nothing stored server-side.
The signed TXs can only do one thing — pay the provider. The provider is not in custody of funds, just holding pre-authorized payment instruments. The blockchain is the single source of truth for access verification.
The provider runs a server that receives signed transactions and broadcasts them when their nLockTime is reached. The TX queue is keyed by dedicated address — no subscriber identity is stored.
4a. Registration endpoint
// POST /api/subscription/register // Receives signed TXs keyed by dedicatedAddress — no subscriber identity app.post("/api/subscription/register", async (req, res) => { const { dedicatedAddress, providerId, transactions } = req.body; // Store in TX queue keyed by dedicated address only txQueue[dedicatedAddress] = { providerId, transactions, status: "pending", registeredAt: Date.now(), startBlock: req.body.startBlock || 0, intervalBlocks: req.body.intervalBlocks || 43200, totalPeriods: req.body.totalPeriods || transactions.length, paymentAmount: req.body.paymentAmount || 0, currency: req.body.currency || "VRSC" }; save(); res.json({ status: "pending" }); });
4b. Broadcast loop
// Iterates over dedicated addresses, NOT subscriber identities // Runs every 5s when TXs are pending, 30s otherwise async function broadcastDue() { const currentBlock = (await rpc("getinfo")).blocks; for (const [dedAddr, entry] of Object.entries(txQueue)) { if (entry.status === "cancelled" || entry.status === "completed") continue; for (const tx of entry.transactions) { if (tx.broadcast) continue; if (tx.lockTime !== 0 && currentBlock < tx.lockTime) continue; try { tx.txid = await rpc("sendrawtransaction", [tx.rawTx]); tx.broadcast = true; entry.status = "active"; } catch (err) { if (err.message.includes("missing inputs")) { entry.status = "cancelled"; // funds swept by subscriber break; } } } if (entry.transactions.every(t => t.broadcast)) { entry.status = "completed"; // all TXs broadcast, auto-purge later } } }
4c. TX queue states
| Status | Meaning | Transition |
| pending | TXs received, waiting for first locktime | First TX broadcasts → active |
| active | Broadcasting in progress | All TXs broadcast → completed |
| completed | All TXs broadcast successfully | Auto-purged after 24h |
| cancelled | Funds swept (subscriber cancelled) | Auto-purged after 24h |
Access control is purely chain-based. The server stores nothing about subscribers. On each check:
// 1. Read subscriber's VerusID to find their dedicatedAddress // height -1 includes mempool — detects subscription metadata before it confirms const identity = await rpc("getidentity", [subscriberId, -1]); const cmm = identity.identity.contentmultimap; const subMeta = JSON.parse(hexDecode(cmm[SUB_ACTIVE_KEY][0])); const dedAddr = subMeta.dedicatedAddress; // 2. Count payments from dedicated address to provider const provId = await rpc("getidentity", [providerId]); const provIAddr = provId.identity.identityaddress; const deltas = await rpc("getaddressdeltas", [{ addresses: [dedAddr] }]); const outTxids = [...new Set(deltas.filter(d => d.satoshis < 0).map(d => d.txid))]; let paymentCount = 0; for (const txid of outTxids) { const rawTx = await rpc("getrawtransaction", [txid, 1]); if (rawTx.vout.some(v => v.scriptPubKey?.addresses?.includes(provIAddr))) paymentCount++; } // 3. Calculate access window const accessUntil = subMeta.startBlock + (paymentCount * subMeta.intervalBlocks); const currentBlock = (await rpc("getinfo")).blocks; if (paymentCount > 0 && currentBlock < accessUntil) { // ACTIVE — serve premium content } else { // Not subscribed or expired } // Nothing stored. Every check is live from the chain.
Detect the Verus Web Wallet and trigger subscriptions from your website:
// 1. Detect the wallet if (window.verus) { console.log("Verus Web Wallet detected"); } // 2. Subscribe (wallet handles terms display, approval, funding, signing) const result = await window.verus.subscribe({ provider: 'myservice.youridentity@', planId: 'basic' // optional - defaults to first plan }); // 3. Send signed TXs to broadcast service — NO subscriber identity await fetch("/api/subscription/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ dedicatedAddress: result.dedicatedAddress, providerId: "myservice.youridentity@", transactions: result.transactions, startBlock: result.startBlock, intervalBlocks: result.intervalBlocks, totalPeriods: result.totalPeriods, paymentAmount: result.paymentAmount, currency: result.currency }) }); // 4. Check access (chain-based — server reads subscriber's VerusID) const status = await fetch("/api/subscription/status?id=subscriber@&providerId=myservice.youridentity@"); // status.status: "active" | "pending" | "expired" | "none" // 5. Cancel (sweeps remaining funds back to subscriber) await window.verus.cancelSubscription({ provider: 'myservice.youridentity@' });
| Feature | VRSC | vETH / Tokens |
| Funding outputs | 1 per period | 2 per period (currency + VRSC fee) |
| Sign unconfirmed | Yes | No — must wait for 1 confirmation |
| Inputs per TX | 1 (vout[i]) | 2 (vout[i*2] + vout[i*2+1]) |
| Output format | {"iAddr": amount} | {"iAddr": {"iCurrId": amount}} |
| Fee deduction | Deduct 0.0001 from payment | Full amount (fee is separate VRSC) |
| Total time | ~5 seconds | ~3-5 minutes (confirmation wait) |
// These keys are deterministic - same on every node veruspay.vrsc::subscription.terms i8iZrgfNEB5c8oEGENRC9B5Cv8Agvv3mqv on provider veruspay.vrsc::subscription.active iCvwWogVjiNCbiKVE38t88MEqFVFfDrjYY on subscriber // Generate your own VDXF keys verus getvdxfid "yournamespace::your.key.name" // Returns: { "vdxfid": "iXxxYyy...", "hash160result": "...", ... }
updateidentity replaces the entire contentmultimap — always read-merge-writeupdateidentity fails if another update for the same identity is unconfirmed — wait for prior updatescreaterawtransaction requires i-addresses, not friendly names — resolve with getidentitysendcurrency returns an opid, not a txid — poll z_getoperationresult for the actual txidname + parent fields in updateidentity, never fullyqualifiednameupdateidentity has a ~5500 byte limit on total contentmultimap data per transaction — keep payloads lightweight, send large data (like signed TXs) off-chaingetaddressdeltas to count actual payments, never rely on local state