Skip to Content
GuidesSandbox projects

Sandbox projects

A sandbox project is a first-class TxNod project whose kind column is 'sandbox' instead of 'production'. Sandbox projects work end-to-end against the real REST API, the real webhook dispatcher, and the real SDK — without on-chain interaction. You drive every state transition with client.sandbox.simulate* and your webhook handler receives real signed events with mode: 'sandbox'. Sandbox projects are never billed, never count against subscription minimums, and are listed separately in the dashboard.

What is a sandbox project?

The projects.kind column discriminates a sandbox project from a production project. Every API key minted against a sandbox project carries the sk_sandbox_ prefix; every webhook envelope dispatched from a sandbox project carries mode: 'sandbox'. There are no separate hosts, no separate database, no separate dashboard — sandbox and production share the same Postgres, the same authorization server, the same dispatcher. Discrimination is structural (the kind column, the secret prefix, the envelope mode), not infrastructural.

Sandbox projects auto-provision testnet xpubs across all seven supported chains at creation time — there is no wallet wizard, no Ledger handshake, no faucet. Pool addresses derive from the auto-provisioned xpubs at indices 1, 2, 3, onward exactly as production projects do; the only difference is that no real funds ever land on those addresses because every “payment” is synthesized by client.sandbox.simulate*.

Create a sandbox project

In the dashboard, navigate to Projects → + New sandbox project at https://txnod.com/projects/new-sandbox . One click provisions the project shell, the seven testnet xpubs, the per-chain bindings, and a sandbox PAT scoped to sandbox:simulate. The success screen surfaces four secrets — copy each into your local .env file or secrets manager:

  • TXNOD_PROJECT_ID — ULID identifying the project (same shape as production project IDs).
  • TXNOD_API_SECRETsk_sandbox_... HMAC secret. The sk_sandbox_ prefix is the SDK’s first-line guard against accidental mainnet routing.
  • TXNOD_WEBHOOK_SECRET — independent per-project secret used to sign outbound webhooks. Different from the API secret (this is the F-16 invariant — production and sandbox each have their own webhook secret; samples wire TXNOD_WEBHOOK_SECRET_SANDBOX as a second env var).
  • Sandbox PAT — Personal Access Token carrying the sandbox:simulate scope. Used by MCP tools and by automated test harnesses that need to drive the simulate-* loop. Cross-tenant guarantee: the 14 sandbox MCP tools each call a 4-invariant guard (row exists / not soft-deleted / kind === 'sandbox' / ownerUserId match) that collapses any miss into a single not-found surface — see Layer 7.

Each secret is shown exactly once. Re-issue from the dashboard if any value is lost.

What you get

  • Auto-provisioned testnet xpubs — one per chain (BTC, ETH, TRON, Cardano, Polygon PoS, BSC, TON), all bound to the project at creation. Pool address allocator works exactly as in production: index 0 is reserved for the handshake (auto-confirmed for sandbox), invoices use indices 1+, cooldown semantics identical.
  • Sandbox-scoped API secretsk_sandbox_... prefixed. The SDK refuses to instantiate TxnodClient with a sandbox secret in a production-detected environment unless you set iAcknowledgeRoutingRealCustomerFundsToSandboxAddresses: true (and even then it logs a non-suppressible console.error and flags every request with X-Txnod-Client-Environment: production).
  • Independent webhook secretTXNOD_WEBHOOK_SECRET for the project. Distinct from the API secret and from any production project’s webhook secret. Sample integrations declare both TXNOD_WEBHOOK_SECRET and TXNOD_WEBHOOK_SECRET_SANDBOX so a single deploy can verify inbound webhooks from either mode.
  • Sandbox PATsandbox:simulate scope. Drives the 14 sandbox MCP tools and the automated test harnesses. Cannot drive simulation against production projects (cross-mode rejection per spec §5.6).

The agent loop

Sandbox projects are designed for an integrate → exercise → verify loop driven by an AI coding agent. The agent reads node_modules/@txnod/sdk/AGENTS.md, installs @txnod/sdk, writes the integration code, then walks all 7 webhook events deterministically using client.sandbox.simulate*. See the Agent-driven testing guide for the complete loop, including a worked Next.js 16 App Router example with /api/checkout + /api/txnod-webhook route handlers.

Drive the state machine

The sandbox surface exposes 14 methods (10 invoice-scoped, 4 project-scoped) — same names as the matching MCP tools (sandbox_simulate_detect, etc.). Each call advances exactly one state transition and emits exactly one webhook event. The 7 transition scenarios cover the full event matrix:

ScenarioCallsEvents emitted
detected → paidsimulateDetect then simulatePaidinvoice.detected, invoice.paid
detected → overpaidsimulateDetect then simulateOverpaidinvoice.detected, invoice.overpaid
detected → partialsimulateDetect then simulatePartialinvoice.detected, invoice.partial
pending → expiredsimulateExpireinvoice.expired
expired → expired_paid_latesimulateExpire then simulateLatePaymentinvoice.expired, invoice.expired_paid_late
paid → reverted → paidsimulateDetect, simulatePaid, simulateReorg, simulateReconfirminvoice.detected, invoice.paid, invoice.reverted, invoice.paid (re-emitted with stable event_id)
Duplicate deliverysimulateDuplicateDeliveryre-fires the most recent terminal event with the SAME event_id

clockAdvance(projectId, { chain, blocks }) increments per-chain confirmation counters across detected invoices — useful for integrations that gate fulfillment on event.data.confirmations.

Reset between runs

client.sandbox.reset(projectId) soft-purges the data tail — invoices, transactions, outbox events, the address pool — while preserving the project shell, the seven xpubs, the chain bindings, the API key, and the sandbox PAT. Idiomatic Vitest usage:

import { TxnodClient } from '@txnod/sdk'; declare const beforeAll: (fn: () => Promise<void>) => void; declare const afterAll: (fn: () => Promise<void>) => void; declare const client: TxnodClient; beforeAll(async () => { await client.sandbox.reset(process.env.TXNOD_PROJECT_ID!); }); afterAll(async () => { await client.sandbox.reset(process.env.TXNOD_PROJECT_ID!); });

The dashboard surfaces a one-click Reset button on the sandbox project detail page that calls the same endpoint.

Auto-cleanup policy

Sandbox projects participate in an automatic cleanup policy (per F-21):

  • 30 days idle → soft-purge. A sandbox project with no API or simulate-* activity for 30 consecutive days has its data tail purged on the same schedule as a manual reset(). The shell, the xpubs, and the bindings stay; the secrets remain valid.
  • 90 days idle → full delete. A sandbox project with no activity for 90 days is fully cascade-deleted, including the shell. Secrets stop working at that point.

The dashboard surfaces a banner on the sandbox detail page when the project is approaching either threshold so an integrator can re-activate (reset() or any simulate-* call resets the idle clock).

Safety

Sandbox secrets and sandbox projects exist in the same Postgres database as production. Discrimination is structural — the sk_sandbox_ prefix, the projects.kind column, the webhook envelope’s mode discriminator, and the SDK’s environment-detection guards. Six independent layers of defense sit between the integrator and accidentally routing real customer funds to a sandbox address.

Per-chain format-safety truth table

For 4 of the 15 supported assets, the chain itself rejects a sandbox→mainnet send because the address format differs between mainnet and testnet. For the remaining 11 assets — every EVM-family chain plus the TRON family — the address format is identical between mainnet and testnet, so format protection is impossible and safety relies on the structural layers below.

ChainMainnet formatSandbox/testnet formatMainnet wallet rejects send?Mechanism
Bitcoinbc1q... (bech32 mainnet HRP) / 1... / 3...tb1q... (bech32 testnet HRP) / m... / n... / 2...YesBech32 HRP byte differs; legacy address version byte differs
Ethereum (ETH + ERC-20 USDT/USDC)0x... 20-byte hex0x... 20-byte hexNoIdentical address format; safety via chain id at signing time
Polygon PoS (POL + Polygon USDT/USDC)0x... 20-byte hex0x... 20-byte hexNoIdentical address format; safety via chain id
BNB Smart Chain (BNB + BEP-20 USDT/USDC)0x... 20-byte hex0x... 20-byte hexNoIdentical address format; safety via chain id
TRON (TRX + TRC-20 USDT)T... (base58check, 0x41 leading byte)T... (base58check, 0x41 leading byte — testnet uses same byte)NoIdentical address format; testnet differentiation is endpoint-only
Cardanoaddr1... (mainnet NetworkId)addr_test1... (testnet NetworkId)YesAddress-level NetworkId byte differs
TON (Toncoin)EQ... / UQ... (mainnet workchain prefix)kQ... / 0Q... (testnet workchain prefix)YesAddress prefix byte encodes workchain + bounceable bit
TON (jetton USDT-TON)EQ... / UQ... mainnetkQ... / 0Q... testnetYesSame workchain-prefix mechanism

Coverage line: 4 of 15 assets format-safe (BTC, ADA, TON, USDT-TON); 11 assets — every EVM-family + TRON family — cannot be format-protected.

Six-layer defense

The structural layers below are what catch the 11 non-format-safe assets and what double-cover the 4 format-safe ones:

  1. API-secret prefix. Every sandbox secret begins with sk_sandbox_. Production secrets use a different (production) prefix. Anyone — human or agent — can grep their .env files for the prefix to verify they match the deployed NODE_ENV.
  2. SDK constructor hard-fail. When the SDK detects NODE_ENV === 'production' (or the explicit environment: 'production' constructor option) AND the configured secret starts with sk_sandbox_, instantiation throws TxnodSandboxKeyInProductionError. Override exists (iAcknowledgeRoutingRealCustomerFundsToSandboxAddresses: true) but is deliberately verbose, logs a non-suppressible console.error, and flags every request with X-Txnod-Client-Environment: production.
  3. SDK xpub-prefix guard. When TXNOD_<chain>_XPUB configured for a production project starts with a testnet prefix (tpub, vpub, upub), the SDK throws TxnodSandboxXpubInProductionError at boot. (Carve-out: EVM-family xpub prefixes are valid on both mainnet and testnet; this layer only catches BTC-family testnet prefixes.)
  4. Server cross-mode rejection. A sandbox API secret targeting a production project (or a production secret targeting a sandbox project) is rejected at the gateway with error_code: sandbox_key_against_production_project (HTTP 400) or production_key_against_sandbox_project. The SDK throws TxnodSandboxKeyAgainstProductionProjectError / TxnodProductionKeyAgainstSandboxProjectError.
  5. Webhook envelope mode. Every outbound webhook carries mode: 'production' | 'testnet' | 'sandbox'. Integrators assert in their handler: if (event.mode === 'sandbox' && process.env.NODE_ENV === 'production') throw new Error('refusing to process sandbox event in production');.
  6. Documentation and CI assertion. The recommended CI assertion (see below) is reproduced byte-exact in the SDK-bundled 05-sandbox.md, this guide, the Sandbox safety standalone reference, and every example app’s tests/setup.ts. An agent or human integrator can grep for the canonical text to verify they’re aligned.

Add this once at the top of your test setup so a misconfiguration breaks CI loudly:

// tests/setup.ts function assertSafeMode(): void { const env = process.env.NODE_ENV ?? 'unknown'; const secret = process.env.TXNOD_API_SECRET ?? ''; const isSandbox = secret.startsWith('sk_sandbox_'); if (env === 'production' && isSandbox) { throw new Error( '[ci] Sandbox API secret cannot run with NODE_ENV=production', ); } if (env !== 'production' && !isSandbox) { throw new Error( '[ci] Production API secret cannot run outside production', ); } } assertSafeMode(); export {};

What this means in practice

For the four chains where the chain itself rejects sandbox→mainnet sends (BTC, ADA, TON, USDT-TON), a mainnet wallet that tries to pay a sandbox address fails at submit time — the wallet displays an “invalid address for this network” error and no funds move. For the other 11 assets — every ETH/Polygon/BSC ERC-20-style asset and every TRON TRC-20 asset — the safety must come from the SDK’s environment-detection layer, the server’s cross-mode rejection, and the webhook envelope’s mode discriminator. The recommended CI assertion (block above) is the single most load-bearing line of defense for those 11 assets — it ensures a sandbox secret cannot run with NODE_ENV=production even if every other layer is bypassed.

For the standalone safety reference with code examples per defense layer and three concrete failure-scenario walkthroughs, read Sandbox safety.

Graduate to production

When you’re ready to ship to mainnet:

  1. Register a hardware-wallet xpub via the production project’s Wallets wizard (Ledger via WebHID, or paste from Ledger Live / Sparrow / Electrum / Cardano CIP-30 browser wallet / TON Connect). Sandbox auto-provisioning does not exist for production projects.
  2. Create a production project with a non-sk_sandbox_ API secret. The dashboard’s + New project button (not the sandbox button) provisions the project shell.
  3. Swap sk_sandbox_... for the production secret in your deployed .env. Keep the sandbox secret in your local dev .env.local so you can keep running the test suite during development.
  4. Drop your event.mode === 'sandbox' assertions that gated test-only behavior — production webhooks carry mode: 'production', so the assertion now fails-closed if a sandbox event ever leaks into production.
  5. Re-read Sandbox safety — confirm every defense layer is wired in your production deploy, not just the sandbox one.

The graduation path is intentionally explicit: you don’t “flip a switch” — you create a new project, register a real xpub, and swap the secret. The sandbox project keeps existing in parallel for ongoing CI runs.