VetoVetoDocs
Guides

Test mode & sandbox

Build and exercise a full checkout with zero real money — the offline mock rail, hosted test keys and mode isolation, the deterministic mock facilitator, and a sandbox loop that mirrors production exactly.

You can drive the entire checkout — discovery, the acceptance gate, settlement, and a signed receipt — without moving a cent. Two layers make this safe, and they compose: the offline mock rail for local testing, and hosted test keys with hard mode isolation for an end-to-end sandbox that mirrors production.

Test mode isn't a stripped-down path. The gate, the receipt, the webhooks, and the reason codes are identical to live — only the money is fake. That's the whole point: what passes in sandbox passes in production.

Layer 1 — the offline mock rail (self-hosted)

The mock rail makes the whole flow runnable with no chain, no network, and no install beyond the SDK. It does structural validation only and always "captures" successfully, so you can exercise the acceptance gate — caps, rate limits, mandate requirements, reputation, intent — deterministically in tests and demos.

gate.test.ts
import { createCheckout, STRICT } from '@veto-protocol/checkout';

const checkout = createCheckout({
  merchant: { id: 'acme', name: 'Acme', domain: 'shop.acme.example' },
  catalog: [{ sku: 'pro', name: 'Pro plan', price: { amount: '49.00', currency: 'USD' }, available: true }],
  receiving: { mock: { account: 'acct_test' } },
  policy: STRICT(), // requires a mandate for ANY spend
});

const created = await checkout.createSession({
  agent_id: '11111111-1111-1111-1111-111111111111',
  items: [{ sku: 'pro', qty: 1 }],
  rail: 'mock',
});

const settled = await checkout.settle(created.body.session_id, { payment: { mock: true } });
console.log(settled.status);            // 'rejected' — STRICT wants a mandate over $0
console.log(settled.body.reason_codes); // ['MANDATE_REQUIRED'] (and/or REPUTATION_TOO_LOW)

rails defaults to [x402, mock] when you omit it, so mock is available out of the box — pass rail: 'mock' when creating a session to use it. The getting-started test-mode page has the full mock-rail walkthrough.

The mock rail never moves money — never set it as a live receiving destination. On the hosted plane, a mock receiving rail on a live merchant is rejected with RECEIVING_MOCK_IN_LIVE.

Layer 2 — hosted test keys & mode isolation

On the hosted control plane, every API key is either test or live, and the mode is baked into the key:

Key prefixlivemodeSettles through
veto_test_…falsethe mock facilitator — a deterministic fake txHash
veto_live_…truethe real CDP facilitator — real USDC on Base mainnet

The isolation is absolute: a test key can never be handed the real on-chain facilitator. Any attempt is rejected with TEST_MODE_RAIL_FORBIDDEN. A test key creates test merchants, reads test orders, and settles on the mock — it cannot reach a live rail by any path.

create a test merchant
curl -X POST https://api.veto-checkout.com/v1/merchants \
  -H "Authorization: Bearer veto_test_…" -H "Content-Type: application/json" \
  -d '{ "slug": "acme-sandbox", "name": "Acme (sandbox)", "domain": "shop.acme.example",
        "receiving": { "x402": { "chain": "base-sepolia", "address": "0xYourAddr…", "asset": "USDC" } } }'

The deterministic mock facilitator

A test-key settle still runs the real signature recovery (ecrecover) — so a test exercises the genuine EIP-3009 crypto path and a SIGNER_MISMATCH in sandbox is a real signing bug you've caught early. Only the broadcast is faked: the facilitator returns a deterministic fake txHash derived from the nonce, so test receipts are stable and replay-safe across runs. You get a complete, signed, verifiable order with no chain.

A sandbox loop that mirrors production

Build on a test key

Create your merchant, catalog, receiving (Base Sepolia), and policy with a veto_test_ key. Everything is identical to live except the key.

Exercise every decision branch

Drive the gate to each terminal outcome and assert on the reason codes — they're the stable contract:

WantHow
accepta small spend under your requireMandateOverUsd, known agent
rejecta spend over a cap, or a blocked agent → OVER_PER_TX_CAP / AGENT_BLOCKED
holda tier below holdForReviewBelowTierHOLD_FOR_REVIEW

See policies & trust for the full rule list.

Verify webhooks against a test endpoint

Register a test webhook endpoint and confirm your handler verifies the signature and is idempotent — the webhooks guide shows the exact verifier. Test deliveries carry "livemode": false in the envelope.

Verify a sandbox receipt

A sandbox order ships a real, signed merchant receipt. Verify it offline — the signature path is identical to live; only the settlement txHash is the deterministic fake.

Promote with one change

When sandbox is green, the only change to go live is the key (and a Base-Sepolia → base receiving flip). Walk the x402 + CDP cutover; nothing about the gate, receipts, or webhooks changes.

Run the bundled example pair

The example pack ships a stub merchant and a buyer agent you can run offline with no install — the fastest way to see the whole loop:

npm run stub   # node --experimental-strip-types examples/stub-merchant/server.ts
npm run buy    # node --experimental-strip-types examples/buyer-agent/buy.ts

The buyer agent runs four scenarios end to end — a small no-mandate purchase (accepted), a larger one (held), a mandate-backed purchase (accepted, premium tier), and a mandate replay (rejected with MANDATE_REPLAY) — so you can watch every branch of the gate without a chain.