The codec for building DigiDollar tooling without a wallet. Encode and decode
DigiDollar addresses (DD…/TD… ↔ the dgb1p…/dgbt1p… taproot form that vouts
actually render), and parse DigiDollar's on-chain OP_RETURN records (mint /
transfer / redeem). Zero dependencies, Node 18+.
Why this exists: DigiDollar is a colored-coin layer over taproot — DD payments are 0-value
witness_v1_taprootoutputs plus anOP_RETURN "DD"record assigning cents to them. DigiByte Core v9.26.4 ships no wallet-level watch-only support for DD and doesn't document the address encoding. This package is that missing layer, reverse-engineered from live chain data and checked against the Core source — so explorers, payment processors, and facilitators can read DigiDollar without holding anyone's keys.
Part of dgb-tools. Testnet-verified; independent community project, not affiliated with the DigiByte Foundation.
npm install dgb-digidollar-codecimport {
decodeDDAddress, encodeDDAddress, // DD/TD address <-> 32-byte x-only taproot key
ddToTaproot, taprootToDD, // DD/TD <-> dgb1p/dgbt1p (same key, other coat)
parseDDOpReturn, // "DD" OP_RETURN -> {type, amounts, ...}
mapDDTransaction, // verbose tx -> which address received how many cents
} from "dgb-digidollar-codec";
decodeDDAddress("TD315gXoEKcy61RtJR5gYXvpWp7YS7ctF8vkoG825hmvR3tdNTWZ");
// { network: 'testnet', xOnlyKey: 'df56ab…6a4f', version: 0xb129, canonical: true }
parseDDOpReturn("6a0244440102016402a523");
// { type: 'transfer', txType: 2, amountsCents: [100, 9125], totalCents: 9225 }
mapDDTransaction(verboseTx, "testnet");
// { record, ddOutputs: [{ n: 0, cents: 100, ddAddress: 'TD315…', taprootAddress: 'dgbt1p…' }, …] }With these four calls a service can watch DigiDollar payments to any address by reading blocks — no wallet, no keys, no custody. That's the verification core of an x402 facilitator, an explorer's DD tab, or a payment notifier.
Address: base58check( version:2B ‖ x-only-pubkey:32B ), sha256d checksum.
The same 32-byte key renders as dgbt1p… (bech32m, witness v1) in transaction
outputs — getrawtransaction shows the taproot form, wallets show the DD form.
| Network | Version | Prefix | Status |
|---|---|---|---|
| testnet | 0xb129 |
TD |
✅ verified against live testnet26 chain data |
| mainnet | 0x5285 |
DD |
✅ verified against an address derived on a mainnet v9.26.4 node on activation day (2026-07-17) — a test vector in test/ |
| regtest | 0xa3a4 |
RD |
OP_RETURN records (from src/digidollar/txbuilder.cpp @ v9.26.4 — note the
transfer format's source comment claims an output_count field; the code
pushes tag, txType, then amounts directly. The code wins, and live transactions
agree):
mint : "DD" 1 <ddAmountCents> <lockHeight> <lockTier> <ownerXOnlyKey:32B>
transfer : "DD" 2 <amountCents>… # one per 0-value taproot vout, in vout order
redeem : "DD" 3 <ddChangeAmountCents>
Amounts are Bitcoin script numbers (minimal little-endian). Test vectors in
test/ are real transactions from testnet26.
- xpub → DD address derivation (BIP86-style) — planned for v0.2, after the wallet's tweak scheme is validated against wallet-minted addresses. Until then, merchants supply addresses; services watch them with this codec.
- Anything that signs. This package reads; it never spends.
MIT.
Every DigiDollar record declares the DD outputs a transaction creates — the zero-value taproot outputs, in order. So a redemption's field is its change, never its burn; what it burns is everything it consumed minus that. A redemption with no record creates no DD output and burns everything it consumed. Getting this wrong overstated one public census by exactly $420 for six weeks.
npm test now runs the parser over all 186 DigiDollar transactions of mainnet
week one (fixtures included; the census they come from reconciles to
getdigidollarstats to the cent) and classifies live explorer JSON for one
transaction of each shape.
import { classifyEsploraTransaction, isCollateralSpend, LOCK_TIERS } from "dgb-digidollar-codec";
const tx = await fetch("https://digiexplorer.info/api/tx/" + txid).then((r) => r.json());
const c = classifyEsploraTransaction(tx);
// c.kind → "mint" | "transfer" | "redeem" | "redeem (no record)" | "not-digidollar"
// c.record → parseDDOpReturn() result (mint: ddAmountCents, lockHeight, lockTier, ownerXOnlyKey)
// c.ddOutputs → [{ n, cents, taprootAddress, ddAddress }] both address forms
// c.ddInputs → [{ txid, vout }] value them by classifying the creating tx (one hop)
// c.collateralSpends → inputs whose control block carries the DigiDollar NUMS internal key
// c.shapeOk → declared amounts match zero-value taproot outputsCollateral is only provable at spend (the NUMS key 50929b74…803ac0 in the
control block). At creation, a valued taproot output in a mint may be collateral
or change — say so rather than guess. Live demo of all of this, in the browser:
dgbinsights.com/lookup.
parseOracleScript(scriptHex) decodes the coinbase OP_RETURN OP_ORACLE <0x03> <payload>
script that carries the DigiDollar price quorum, exactly as DigiByte Core v9.26.5
serializes and validates it (src/oracle/bundle_manager.cpp, src/oracle/musig2_aggregator.cpp):
bitmap_len(1) · bitmap · epoch(4 LE) · price(8 LE, micro-USD) · timestamp(8 LE) · aggregate_sig(64).
decodeParticipationBitmap(hex) applies Core's rules: length must equal ⌈roster/8⌉, bits above
the roster must be zero, bit b of byte j is slot 8j+b. findOracleBundle(tx) locates the
script structurally in an Esplora-shaped coinbase; it never searches output hex for bf.
The decoded slot set is the signing set the aggregator chose for that epoch — it records
participation in signing, not liveness.