How the DigiDollar Toolkit works, for a developer who wants to accept DigiDollar (DD) and DigiByte (DGB) without a hosted payment gateway.
New to DigiDollar? Start with WHAT_IS_DIGIDOLLAR.md, then come back here. Ready to run a node? Jump to the operator playbook, guides/ACCEPTING_DIGIDOLLAR_AND_DGB.md.
DigiDollar is exact USD on-chain: 1 DD = $1.00, by definition. No oracle, no
rate-lock, no volatility window. That single fact — plus a handful of DigiByte
Core protocol realities — shapes every layer of this toolkit. This document walks
through the mental model, the two ways you can wire it up, the pieces of the
library, and the exact sequence a payment travels through.
A DigiDollar checkout involves four cooperating parts. Keeping them separate is the whole design.
flowchart LR
subgraph Customer
B["Browser<br/>checkout page"]
end
subgraph You["Your infrastructure"]
direction TB
BR["Brand / storefront<br/>(the product you sell)"]
BE["Backend<br/>(edge functions / API + DB)"]
N["DigiByte Core node<br/>(hot DD wallet, localhost RPC)"]
end
B -- "create invoice" --> BE
BE -- "invoice + DD address + amount" --> B
B -- "pays on-chain (wallet)" --> N
N -- "detects DD, POST /ingest (shared secret)" --> BE
N -- "refills address pool (cron)" --> BE
BE -- "credits membership / order" --> BR
- Brand — your storefront and the thing a customer is buying (for the real-world example, BlockIndex.AI's $350 lifetime membership). The brand doesn't know or care about UTXOs; it just wants a "paid / not paid" signal.
- Node — a DigiByte Core node running the DigiDollar build. It holds keys,
mints receive addresses, and is the only component that can authoritatively
see a DigiDollar payment. It lives on a machine you control, with its RPC
bound to
127.0.0.1. - Backend — your web-facing API (Supabase edge functions, an Express app, a Node script — anything). It creates invoices, stores their state, and exposes a secret-gated ingest endpoint the node posts confirmed payments to. The backend never holds a wallet or a private key.
- Browser — the customer's checkout page. It asks the backend to create an invoice, shows the address + amount, and polls for status until it flips to paid.
The important boundary: keys and chain-watching live with the node; web traffic lives with the backend; the two meet only at a small, authenticated ingest contract. The backend can be fully public and hold no secrets that could move money.
The toolkit supports two topologies. They differ only in who initiates the detection handoff. Both post to the same ingest contract and preserve the same invariants.
Your node runs DigiByte Core's walletnotify and blocknotify hooks. On each
wallet transaction, a walletnotify script resolves the payment and POSTs it to
your backend's ingest endpoint. On each block, a blocknotify script re-reports
open payments so their confirmation count climbs. No watcher, wallet, or RPC
client ever runs on the backend.
Reference implementation: guides/walletnotify-reference/
(walletnotify.mjs, blocknotify.mjs, refill-dd-pool.mjs, .env.example).
One wrinkle DigiDollar forces:
walletnotifyis not guaranteed to fire on a DigiDollar receive, because the value lives in anOP_RETURNand the output is 0-satoshi. So the authoritative DigiDollar path isblocknotify→ polllistdigidollarunspent, notwalletnotifyalone.
If you'd rather not push from the node, run a small worker next to the node that
polls listdigidollarunspent (and explorers for BTC/LTC) on a timer, reconciles
each open invoice, and posts confirmed payments to the same ingest endpoint.
Reference implementation: guides/watcher-reference/
(watcher.mjs).
Push is preferred — it needs no always-on worker and keeps the backend wallet-free — but pull is a drop-in alternative when a push hook is inconvenient.
flowchart TB
subgraph PUSH["PUSH model"]
n1["Core node"] -- "walletnotify / blocknotify scripts" --> i1["POST /ingest"]
end
subgraph PULL["PULL model"]
w["watcher worker<br/>(polls on a timer)"] -- "reconcileInvoice() → post" --> i2["POST /ingest"]
w -. "listdigidollarunspent" .-> n2["Core node"]
end
For BTC, LTC, and DGB coin, you can derive a fresh receive address from a cold xpub with pure math — no node required, keys never online. DigiDollar is different, and the difference is a protocol constraint, not a choice:
- DigiByte Core v1 has no DigiDollar watch-only. The node that detects DigiDollar must hold the keys. There is no "import this xpub and watch it" path for DD.
- Addresses come from the node via
getdigidollaraddress, which mints a fresh, HD-derivedDD…(mainnet) /TD…(testnet) /RD…(regtest) address per call — Base58Check, not bech32.
But your public backend usually can't reach the private node (and shouldn't). So the node runs slightly ahead of demand:
- A pool-refill cron on the node calls
getdigidollaraddressrepeatedly and pushes the resulting address strings (never keys) to a secret-gatedpool-refillendpoint on your backend. - The backend stores them in an address pool table.
- When a customer starts a DigiDollar checkout, invoice creation pops one address from the pool and locks it to that invoice.
Because addresses are minted ahead of time and only public strings cross the wire, the node stays private and the backend never needs RPC access. Guard the refill endpoint with its own secret — a poisoned pool would redirect customer funds.
DigiDollarAddressSource (in src/address/digidollar.ts) is the library's
node-facing minter; it validates the network prefix and refuses to hand back a
cross-network address. The pool itself is backend state — the demo and BlockIndex
both model it as a simple table/JSON of unclaimed addresses.
End to end, a single DigiDollar payment travels like this:
sequenceDiagram
autonumber
participant Cust as Browser
participant BE as Backend (edge fns)
participant Pool as DD address pool
participant Node as DigiByte Core node
participant Brand as Membership / order
Note over Node,Pool: cron: getdigidollaraddress → pool-refill (public strings only)
Node->>Pool: top up unclaimed DD addresses
Cust->>BE: POST create-invoice { priceUsdCents }
BE->>Pool: claim one DD address
BE->>BE: buildInvoiceTerms() — lock cents + confs + expiry
BE-->>Cust: { address, amount (cents), confirmationsRequired }
Cust->>Node: pay on-chain (DD in OP_RETURN, fee in DGB)
Note over Node: blocknotify → poll listdigidollarunspent
Node->>BE: POST /ingest { asset, network, address, txid, vout, amount, confirmations } (shared secret)
BE->>BE: re-verify amount + address; evaluatePayment()
loop each new block
Node->>BE: POST /ingest (confirmations ↑)
end
BE->>Brand: confirmations ≥ required → credit once (idempotent)
Cust->>BE: GET status → "paid"
In prose:
- Create invoice. The browser posts a USD target (
priceUsdCents). The backend claims a pool address, callsbuildInvoiceTerms()to lock the expected cent amount, the required confirmations (USD-tiered), and a 20-minute expiry, and returns the address + amount to show the customer. - Customer pays. Their wallet sends DigiDollar to the address. The DD value
rides in an
OP_RETURNon a 0-satoshi Taproot output; the miner fee is paid in DGB, not DD (say so in your UI). - Node detects. On the next block, the node polls
listdigidollarunspent, finds the receive, andPOSTs it to the backend's ingest endpoint over TLS with a shared secret. - Confirmations climb. Each subsequent block re-reports the payment with a higher confirmation count.
- Credit once. When confirmations reach the required threshold and the amount
re-verifies server-side, the backend credits the membership/order — idempotent
by
(asset, network, txid, vout), so replayed notifications never double-credit.
The library is @blockindex/crypto-payments —
pure TypeScript (@scure/@noble), ESM, no ambient clock or randomness in the
core, so the decision logic is deterministic and trivially testable. It does
not grant memberships, own a database, or know about any specific backend.
Everything is re-exported from src/index.ts.
| Module | Path | Responsibility |
|---|---|---|
| assets | src/assets.ts |
Asset + network specs: decimals, address prefixes, which assets are xpub-derivable, and the identityUsd flag that marks DigiDollar as exact-USD. |
| amounts | src/amounts.ts |
toBaseUnits / fromBaseUnits / formatAmount — the integer base-unit (BigInt) conversions. Never floats. |
| engine | src/engine.ts |
The pure payment engine: quoteAmount (USD cents → base units; identity for DD), classifyPayment (exact / underpaid / overpaid / dust), and requiredConfirmations (the USD-tiered policy). No I/O. |
| address/ | src/address/ |
Receive-address sources. xpub.ts derives BTC/LTC/DGB addresses from a cold xpub (deriveXpubAddress, XpubAddressSource); digidollar.ts mints DD addresses from the node (DigiDollarAddressSource); types.ts is the shared AddressSource interface. |
| watch/ | src/watch/ |
Chain watchers that turn on-chain state into DetectedPayment[]. core-digidollar.ts (DigiDollarWatcher, via listdigidollarunspent), core-dgb.ts (DgbCoinWatcher), esplora.ts + explorer-btc.ts / explorer-ltc.ts (public explorers). |
| price/ | src/price/ |
PriceSource implementations. identity.ts (DigiDollar = $1, the no-op quote), node-oracle.ts (DGB/USD from the on-chain oracle), coingecko.ts (+ FallbackPriceSource), blockindex.ts. |
| invoice | src/invoice.ts |
The pure invoice state machine: buildInvoiceTerms (lock amount + confs + expiry), isQuoteExpired, evaluatePayment (is this payment creditable?). Caller passes now — fully deterministic. |
| notify/ | src/notify/core.ts |
The testable core of the push scripts: parseCoreReceives (parse gettransaction for BTC/LTC/DGB) and digiDollarReceivesForTxid (select DD rows from listdigidollarunspent). |
| digibyte-core/rpc | src/digibyte-core/rpc.ts |
Typed DigiByte Core RPC client (DigiByteCoreRpc) and the DigiDollar RPC row types. The only component that talks to the node. |
| worker/reconcile | src/worker/reconcile.ts |
reconcileInvoice(terms, payments) — drives the watcher loop, returning 'credit' | 'wait' | 'underpaid' | 'double_spend_risk' | 'none' plus a suggested invoice status. |
A typical backend uses assets + engine + invoice + reconcile for decisions, address/ to mint/claim, and either watch/ (pull) or notify/ (push) to observe the chain. digibyte-core/rpc is only imported where you actually touch the node.
Every amount in the toolkit is an integer number of base units, as a BigInt:
- DigiDollar — the base unit is the US cent.
$350.00is35000ncents. Quoting is the identity function (quoteAmountreturnsBigInt(priceUsdCents)), because there is no rate to look up. - BTC / LTC / DGB — the base unit is the satoshi (or DGB's smallest unit).
quoteAmountconverts a USD target into base units with ceil division so the buyer always covers the target, and a tolerance band absorbs the sub-unit remainder.
Comparisons (classifyPayment, evaluatePayment) are pure BigInt. Floats are a
money bug — a fraction of a cent silently lost or rounded the wrong way is a real
loss or a wrongly-rejected invoice. The library never introduces a float into an
amount path, and neither should your backend.
requiredConfirmations(asset, priceUsdCents) encodes a USD-tiered policy — the
more money at stake, the more confirmations before crediting:
| Asset | < $100 |
$100 – $10,000 |
> $10,000 |
|---|---|---|---|
| DigiDollar / DGB | 6 (≈ 90 s) | 20 (≈ 5 min) | 60 |
| BTC | 1 | 2 | 2 |
| LTC | 4 | 6 | 6 |
buildInvoiceTerms bakes the required count into the invoice at creation time, so
the threshold is fixed for the life of that invoice. evaluatePayment only marks a
payment creditable once confirmations >= confirmationsRequired, the amount is
exact-or-over, and there's no unconfirmed RBF risk. 0-conf and RBF-pending are
never trusted — they surface as double_spend_risk until buried. The thresholds
are env-overridable at the operator layer for teams with a different risk appetite.
- WHAT_IS_DIGIDOLLAR.md — the concept, from zero.
- guides/ACCEPTING_DIGIDOLLAR_AND_DGB.md — the node-operator + integrator playbook (node config, wallets, the RPC surface, hot-wallet hardening, the security checklist).
- HOW_BLOCKINDEX_INTEGRATED.md — a real production integration built on exactly this architecture.
- examples/demo-merchant/ — a runnable ~300-line reference merchant that implements the whole loop.
- SECURITY.md · FAQ.md · RESOURCES.md