DrawBound is a self-custodial native-BTC credit protocol for testnet. BTC collateral is represented by a TAURUS vault reference; a fresh HAT/RIP-shaped loan-health proof is the covenant that authorizes a SatVM credit transition. Unhealthy or stale evidence freezes new draws, while repayment and the documented unilateral exit path remain available.
corepack pnpm install
corepack pnpm dev # http://localhost:3000 — fixture mode on signetThe app is two pages:
| Route | What it is |
|---|---|
/ |
Landing page — what the protocol claims, with links into the terminal |
/vault |
Vault Terminal — the actual self-custodial session flow (connect, draw, repay, unlock) |
Every flow described below happens in /vault.
Verify everything:
corepack pnpm typecheck # tsc --noEmit
corepack pnpm lint # eslint (flat config)
corepack pnpm test # 109 unit + integration tests
corepack pnpm build # production build
corepack pnpm smoke # HTTP end-to-end run against SMOKE_BASE (default http://127.0.0.1:3107)-
Connect — the browser generates an ephemeral Schnorr keypair (
@noble/curves, BIP-340). The private key never leaves the device; the public key is registered withPOST /api/wallet/connecttogether with the vault ref. The server reads the vault's real locked VTXOs from the Tachi signet daemon, creates (or restores, if previously connected) the position, and returns a session token. -
Act — every draw/repay/unlock signs the canonical message
DrawBound:v1:<positionId>:<vaultRef>:<action>:<amount>:<nonce>with the session key and presents the session token (
x-drawbound-sessionheader) plus the signature. The server verifies the signature, checks the nonce against position state (stale/replayed requests get409), evaluates the covenant, and records an independently hashed receipt. -
Replay safety — the idempotency fingerprint is
positionId:action:amount:nonce. A retried signed request returns the original receipt without re-executing; after success the nonce advances, so old signatures are permanently stale.
Honest scope of session auth: it authenticates the browser session that connected a vault — it does not by itself prove on-chain vault ownership. Ownership is enforced at the chain level in live mode, where the credit transition must be a real Taurus-signed transaction for that vault; an explicit BIP-322 ownership proof can additionally prove key control at connect time.
Connecting can prove control of the vault's user key:
- Request a single-use challenge:
POST /api/wallet/challenge {vaultRef}(or the Request Challenge button in the terminal). - Sign it with the vault user key (BIP-322 simple signature) and note the key's key-path P2TR ownership address.
- Connect with
ownershipNonce,ownershipAddress,ownershipSignature. The server consumes the challenge (single-use; wrong-ref attempts don't burn it) and verifies strictly — no loose BIP-137.
Sessions then carry ownershipVerified: true. With REQUIRE_OWNERSHIP_PROOF=true, connecting a P2TR vault address without a valid proof is refused. Deriving both the vault address and the ownership address from the same user key (see operator tooling) makes the binding by construction.
pnpm exec tsx scripts/operator-live.mts derive # vault P2TR + ownership address from your key (live signet quorum)
pnpm exec tsx scripts/operator-live.mts export-key # mnemonic -> OPERATOR_PRIVATE_KEY (hex + WIF) for the step below
pnpm exec tsx scripts/operator-live.mts register <txid> [vout] # register confirmed L1 funding UTXO on Tachi ledger (TxVaultOpen)
pnpm exec tsx scripts/operator-live.mts ownership <vaultRef> # challenge -> signed connect body
pnpm exec tsx scripts/operator-live.mts status <vaultRef> # locked-VTXO read
pnpm exec tsx scripts/operator-live.mts fund-help # funding + live-write procedure
corepack pnpm live:onboard # sequenced driver: derive -> fund -> register -> check -> build (stops before the write)
corepack pnpm live:check # read-only preflight: every gate + a daemon probe (exit 1 when blocked)
corepack pnpm live:build -- <fundingTxid> <amountSats> # build/verify a signed transition hex, never broadcast itderive takes a mnemonic but ownership needs a 32-byte hex key, and the wallet
aggregator never exposes private keys — export-key is the bridge. Confirm its
ownershipAddress matches the one derive printed before connecting.
Reference oracle for strict proof mode (holds the signing key; DrawBound only ever sees the public key):
ORACLE_PRIVATE_KEY=$(openssl rand -hex 32) pnpm oracle
# then: HAT_ORACLE_URL=http://127.0.0.1:3109 PROOF_RELAY_PUBLIC_KEYS=<oraclePubkey> corepack pnpm dev| Mode | Reads | Writes | Purpose |
|---|---|---|---|
FIXTURE (default) |
live signet reads (best-effort) | deterministic fixture transitions | rehearsal, demos, CI |
LIVE |
real locked-VTXO state, fail closed on a failed read | broadcasts operator-supplied signed txHex, then waits for daemon commit |
funded signet/regtest validation |
NATIVE and RELAY remain reserved integration boundaries for verified official Tachi APIs; RECORDED (replay of a live trace) is not implemented.
LIVE_TACHI_ENABLED=true
KILL_SWITCH=false # live writes are refused while the kill switch is engaged
TACHI_VAULT_REF=tb1p<your-funded-signet-taurus-vault>
ALLOWED_VAULT_REFS=tb1p<your-funded-signet-taurus-vault>
corepack pnpm live:check # read-only; prints exactly what still blocks a broadcast
corepack pnpm devSafety gates — all fail closed:
- Testnet only:
TACHI_NETWORKmust besignet/regtest; mainnet is refused unlessALLOW_MAINNET=true+LIVE_TACHI_ENABLED=true+ kill switch off. - Kill switch:
KILL_SWITCH=true(the default) blocks every live broadcast. - Vault allowlist: only
ALLOWED_VAULT_REFSentries can connect/read/transition in live mode. - Real transactions only: the live adapter rejects the synthetic demo payload (tagged with the
dbdemo01magic prefix) and any hex that is not a plausibly-sized serialized transaction. A live draw/repay/unlock without a real Taurus-signedtxHex(paste it in the terminal's Advanced box) returns aDENYreceipt. - Right chain, verified: before any live decision the daemon must answer
/tachi_nodeInfowith the chain id forTACHI_NETWORK(tachi-signet-1/tachi-regtest-1); a daemon that advertises nothing is refused, and only loopback hosts are exempt. Attestations are cached forLIVE_ATTESTATION_TTL_MS.TACHI_VAULT_REF/ALLOWED_VAULT_REFSmust decode as bech32m witness-v1 addresses with the network's prefix — the same decoder backs the ownership-proof address check, sotb1p…garbage can't pass as "a taproot address". - No modeled collateral in live mode: with
LIVE_REQUIRE_CHAIN_READ=true(default) a failed or truncated locked-VTXO read refuses the transition instead of substituting fixture collateral. - Pre-broadcast decode + fee ceiling: the hex is sent to
/tachi_txDecodefirst; it must decode, must not exceedLIVE_MAX_FEE_SATS, and its outputs must cover the obligation this transition commits (amount × CREDIT_UNIT_SATS)./tachi_txValidateis deliberately not used — current daemons reject transactions they themselves committed, so its verdict is not evidence. - Commit is confirmed, not assumed: after
POST /tachi_txBroadcastSync(whereresult.code != 0means mempool rejection), the adapter polls/tachi_tx?hash=untilstate == "committed". Only then is the ledger moved; a timeout leaves the position untouched and journals the pending tx hash. - Exposure cap applies to live too:
MAX_TEST_SATSbounds the obligation a live transition may commit, and it is enforced rather than bypassed by the real transaction. - Signed proofs (optional strict mode): set
PROOF_RELAY_PUBLIC_KEYSto require every health proof to carry a valid BIP-340 signature from an allowlisted oracle key; unsigned server-derived attestations are then rejected. ConfigureHAT_ORACLE_URLto fetch such attestations. - Admin gate:
POST /api/resetrequiresx-admin-tokenmatchingADMIN_TOKEN; withADMIN_TOKENunset the route is refused in every mode (fail closed), except whenALLOW_INSECURE_RESET=trueis set for a local demo.
source: "oracle"— signed attestation fromHAT_ORACLE_URL(when configured).source: "derived"— server-computed from real chain state:healthBps = collateralSats / (debtUnits × CREDIT_UNIT_SATS) × 10000, capped at 650%. Derived proofs are debt-aware: drawing reduces health, and the gate re-derives at decision time (no stale snapshots).source: "fixture"— deterministic rehearsal artifacts (healthy/unhealthy/stale/invalid), labeled as such, with no claim of proof security.
The draw gate (evaluateDraw) fails closed on: staleness, position/vault/network/covenant binding, minimum health, credit limit, draw caps, and nonce mismatch.
All state-changing routes are rate-limited per IP and require a session; reads are public.
| Route | Method | Auth | Purpose |
|---|---|---|---|
/api/wallet/challenge |
POST | — | issue a single-use BIP-322 ownership challenge |
/api/wallet/connect |
POST | — | register session pubkey (+ optional ownership proof), create/restore position |
/api/wallet/disconnect |
POST | session | revoke session |
/api/draw · /api/repay · /api/unlock |
POST | session + signature | covenant-gated transitions |
/api/proofs |
POST | session | refresh the position's health attestation |
/api/positions |
GET / POST | — / admin(live) | session-scoped position, public list; seed a fixture position |
/api/receipts |
GET | — | decision audit trail (session-scoped by default) |
/api/reset |
POST | admin | destructive demo reset |
/api/tachi/diagnostics |
GET | — | read-only Tachi network inspection + live-readiness gates (?probe=1 adds a daemon probe; rate-limited) |
/api/health |
GET | — | liveness, mode, policy, counts, and the config-only live-gate summary |
A LIVE denial may also carry pendingTxHash + reconcile: true: the daemon accepted the
broadcast but had not reported it committed. The position stays untouched (frozen), the
refusal is journaled against that nonce, and retrying the signed request replays the
journal instead of broadcasting twice — reconcile with scripts/operator-live.mts status.
Two interchangeable backends behind one StorageBackend interface, chosen with DB_BACKEND:
json(default) — atomic serialized JSON-file writes atDATA_DIR/drawbound.json; single instance.sqlite— Node 24's built-innode:sqlite(zero dependencies), WAL journal, transactional, indexed receipts; auto-imports an existing JSON store on first open and archives it.
All writes are awaited before a decision is returned. See docs/deployment.md for Docker, CI, environment reference, and the production hardening checklist; docs/credit-economics.md records the credit-model decisions (unit definition, lender stance, zero interest, freeze-only risk policy).
- docs/deployment.md — configuration, Docker, CI, production checklist
- docs/security-review.md — security findings from review and how each was fixed
- docs/credit-economics.md — credit model decisions and open product questions
- docs/tachi-integration.md — SDK/Taurus integration record and live-write procedure
- docs/threat-model.md — trust assumptions and what the gates do (and do not) prove
- docs/demo-script.md — short demo flow
The TypeScript SDK (@tachibtc/tachi-sdk-ts@0.2.1) and Taurus packages (@tachibtc/taurus-vault-core@0.3.4, @tachibtc/taurus-wallet-aggregator@0.4.5) are pinned from public npm. Read-only inventory spikes: corepack pnpm spike:tachi / spike:taurus. Mainnet writes are disabled by default; never commit keys.