H
hustle.bitcoins@
VRSC Subscription
J
joe.bitcoins@
vETH Subscription

How It Works

Subscribe to any VerusID provider in 3 steps:

1
Click Subscribe — The Verus Web Wallet reads the provider's terms from their VerusID and shows you the plan details, cost, and duration.
2
Confirm & Sign — The wallet creates a dedicated address, funds it with your payments, and pre-signs time-locked transactions. Everything is stored on your VerusID.
3
Automatic Payments — The provider broadcasts each payment as its time-lock expires. The blockchain enforces the schedule. Cancel anytime by sweeping remaining funds.

Security & Privacy

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.


Identity Explorer

Developer Guide — Build Your Own

Complete guide to building decentralized subscriptions on Verus. No smart contracts needed — just VerusID contentmultimap, nLockTime, and standard RPC calls.

Step 1: Create a Provider VerusID

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

Step 2: Publish Subscription Terms

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.

Step 3: Build the Subscriber Wallet Flow

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
  })
});

Privacy-First Design

The subscription system is designed so the provider never stores subscriber identities. Two components with separate concerns:

Broadcast Service

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.

Access Verification

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.

Step 4: Build the Broadcast Service

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

StatusMeaningTransition
pendingTXs received, waiting for first locktimeFirst TX broadcasts → active
activeBroadcasting in progressAll TXs broadcast → completed
completedAll TXs broadcast successfullyAuto-purged after 24h
cancelledFunds swept (subscriber cancelled)Auto-purged after 24h

Step 5: Chain-Based Access Verification

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.

Step 6: Website Integration

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@'
});

VRSC vs Non-VRSC Currencies

FeatureVRSCvETH / Tokens
Funding outputs1 per period2 per period (currency + VRSC fee)
Sign unconfirmedYesNo — must wait for 1 confirmation
Inputs per TX1 (vout[i])2 (vout[i*2] + vout[i*2+1])
Output format{"iAddr": amount}{"iAddr": {"iCurrId": amount}}
Fee deductionDeduct 0.0001 from paymentFull amount (fee is separate VRSC)
Total time~5 seconds~3-5 minutes (confirmation wait)

VDXF Key Reference

// 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": "...", ... }

Common Pitfalls

  • Never store subscriber identity names on the provider server — use dedicated addresses as queue keys
  • updateidentity replaces the entire contentmultimap — always read-merge-write
  • updateidentity fails if another update for the same identity is unconfirmed — wait for prior updates
  • createrawtransaction requires i-addresses, not friendly names — resolve with getidentity
  • sendcurrency returns an opid, not a txid — poll z_getoperationresult for the actual txid
  • Currency UTXOs cannot be signed unconfirmed — wait for at least 1 confirmation
  • Use the name + parent fields in updateidentity, never fullyqualifiedname
  • updateidentity has a ~5500 byte limit on total contentmultimap data per transaction — keep payloads lightweight, send large data (like signed TXs) off-chain
  • Access verification should always be chain-based — use getaddressdeltas to count actual payments, never rely on local state