VetoVetoDocs
Getting Started

Set up Veto end-to-end (hosted)

Zero to a live agent purchase on the hosted platform — sign in, mint keys, create a merchant, add a product, set your receiving address, publish, connect your site, and watch an AI agent buy it over x402.

This is the whole path on the hosted platform at https://merchants.veto-ai.com: from a fresh account to a real AI-agent purchase that settles USDC on Base. Nothing to host, nothing to install on your servers — you mint a key, describe what you sell, publish, and an agent buys it.

Veto governs · the rail executes.

The control plane verifies the buyer's mandate and runs your acceptance policy before any money moves; the x402 rail only ever executes a spend the gate already approved. You bring your own receiving address — Veto is non-custodial and never holds your funds.

Want the model before the curls?

How Veto works — discover, pay, deliver is the whole arc in plain English, including the part this guide stops short of: after the agent pays, you deliver the goods — triggered by a signed order.settled webhook on every sale.

Test vs live, in one line

Everything in this guide works in two modes, and the mode is decided entirely by which key you use:

veto_test_ keyveto_live_ key
Createsa test merchant (livemode: false)a live merchant (livemode: true)
Settles viathe mock facilitator — structural checks, no real fundsthe url-mode x402-rs facilitatorreal USDC on Base
Receiving chainbase-sepoliabase
Use it tobuild and rehearse the full flow safelytake real money

The two are fully isolated. A test key can never reach a real chain, and a live key never falls through to the mock — so you can develop against the sandbox with total confidence the shapes are identical in production. Build the whole thing with veto_test_ first; flipping to live is the last step.

1. Sign in and get your API keys

Open https://merchants.veto-ai.com and sign in (magic link). Go to Dashboard → Developers and mint two keys:

  • a veto_test_… key — your sandbox. Spends settle on the mock facilitator: the full acceptance gate runs, a signed receipt is issued, but no real funds move.
  • a veto_live_… key — real money. Spends settle on the url-mode x402-rs facilitator, which moves real USDC on Base with no Coinbase dependency.

The secret is shown once.

Each key's full secret (veto_test_… / veto_live_…) is displayed exactly once, at creation. Copy it into your secret manager immediately — Veto stores only a hash and the last4, so a lost secret can't be recovered, only rotated. Treat a veto_live_ key like a production credential.

Confirm the key resolves and check which mode you're in:

who am I
curl https://merchants.veto-ai.com/v1/me \
  -H "Authorization: Bearer veto_test_8f2c…"

Every call below is the same pattern: Authorization: Bearer <your key>. The mode is the key — there's no livemode flag to pass.

2. Create a merchant

A merchant is the top-level resource an agent discovers and buys from. Creating one mints its Ed25519 receipt-signing identity server-side (the private seed never leaves the server and is never returned). livemode is inherited from the key — a test key here makes a test merchant.

POST /v1/merchants
curl -X POST https://merchants.veto-ai.com/v1/merchants \
  -H "Authorization: Bearer veto_test_8f2c…" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "acme",
    "name": "Acme Corp",
    "domain": "shop.acme.example",
    "rails": ["x402", "mock"]
  }'

The response includes the merchant id (mrch_…). You'll use that id — or the slug — in the public /m/<id>/… agent endpoints later, so keep it handy.

rails defaults to ["x402","mock"] when omitted. A live merchant can't enable the mock rail (it's rejected with RECEIVING_MOCK_IN_LIVE) — live merchants settle over x402 only.

3. Add a product

Add a CatalogItem. The price is an exact decimal string, never a float — money is exact end to end.

POST /v1/catalog
curl -X POST "https://merchants.veto-ai.com/v1/catalog?merchant_id=mrch_01J…" \
  -H "Authorization: Bearer veto_test_8f2c…" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "rpt-001",
    "name": "Market Report",
    "description": "Q3 market intelligence, delivered on settle.",
    "price": { "amount": "5.00", "currency": "USD" },
    "available": true,
    "category": "reports",
    "unit": "each"
  }'

?merchant_id= selects which merchant the write targets (omit it to hit your project's default merchant). sku is unique per merchant. For a first real-money smoke test, a cheap product (say "0.01") keeps the live settlement tiny.

4. Set your receiving address

receiving is your pay_to — where settled USDC lands. It's validated at write time, so a broken destination fails for you now, never for an agent mid-checkout. This is the non-custodial core: your address, not Veto's.

For a test merchant, point at Base Sepolia — testnet, no real value:

PUT /v1/receiving (test)
curl -X PUT "https://merchants.veto-ai.com/v1/receiving?merchant_id=mrch_01J…" \
  -H "Authorization: Bearer veto_test_8f2c…" \
  -H "Content-Type: application/json" \
  -d '{
    "receiving": {
      "x402": {
        "chain": "base-sepolia",
        "address": "0x1111111111111111111111111111111111111111",
        "asset": "usdc"
      }
    }
  }'

For a live merchant, point at Base mainnet with your real USDC address — this is where real money will arrive:

PUT /v1/receiving (live)
curl -X PUT "https://merchants.veto-ai.com/v1/receiving?merchant_id=mrch_01J…" \
  -H "Authorization: Bearer veto_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "receiving": {
      "x402": {
        "chain": "base",
        "address": "0xYourRealBaseUsdcAddress…",
        "asset": "usdc"
      }
    }
  }'

The address must be a 20-byte 0x hex address; supported (chain, asset) pairs are USDC on base and base-sepolia. A bad value is rejected loudly with RECEIVING_ADDRESS_INVALID or RECEIVING_CHAIN_UNSUPPORTED.

5. Publish

Your changes are a draft until you publish. POST /v1/publish validates the assembled config (merchant + receiving + catalog + the newest policy version), then activates it and rebuilds the discovery manifest in one transaction.

POST /v1/publish
# Optional dry run — see findings with no side effects:
curl -X POST "https://merchants.veto-ai.com/v1/validate?merchant_id=mrch_01J…" \
  -H "Authorization: Bearer veto_test_8f2c…"

# Publish — goes live:
curl -X POST "https://merchants.veto-ai.com/v1/publish?merchant_id=mrch_01J…" \
  -H "Authorization: Bearer veto_test_8f2c…"
200
{ "published": true, "ok": true, "active_policy_version": 1 }

If validate returns { "ok": false, "findings": [...] }, fix what the reason codes point at (usually receiving or catalog) and re-run. The endpoint returns 200 either way — the findings are the payload.

6. Connect your site

Your merchant is now discoverable. The public manifest is the single bootstrap URL an agent starts from — it advertises your catalog URL, checkout URL, rails, pay_to, and a policy summary, all from relative paths so it works under any host:

the discovery anchor
curl https://merchants.veto-ai.com/m/mrch_01J…/.well-known/agentic-checkout.json

To serve it under your own brand (e.g. shop.acme.example), point a CNAME at Veto so the discovery anchor resolves on your domain. The full recipe — DNS, both URL forms, and verifying discovery — is in Add agentic checkout to your site.

7. Let an agent buy it (x402)

Now the buyer side. This is the exact hosted flow a real agent runs, in path form (/m/<id>/…). On the hosted origin, /m/* and /v1/* route to the API; everything else is the dashboard — so always use the /m/<id>/… form for agent traffic.

Discover the catalog

curl https://merchants.veto-ai.com/m/mrch_01J…/agent/catalog

Pick a sku from items[]. (No auth — the agent endpoints are public.)

Open a checkout → read the payment requirement

agent_id must be a UUID — it anchors reputation and rate limits. Generate one with uuidgen | tr 'A-Z' 'a-z'.

POST /m/<id>/agent/checkout
curl -X POST https://merchants.veto-ai.com/m/mrch_01J…/agent/checkout \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "11111111-1111-1111-1111-111111111111",
    "items": [{ "sku": "rpt-001", "qty": 1 }]
  }'

The response carries a session_id and a self-describing payment_required:

{
  "session_id": "sess_…",
  "payment_required": {
    "payTo": "0xYourReceivingAddress…",     // your address, from step 4
    "amount": "5.00", "currency": "USD",
    "amountBaseUnits": "5000000",            // exact integer base units (USDC = 6 dp)
    "chain": "base",                          // "base" live, "base-sepolia" sandbox
    "asset": "USDC",
    "eip712Domain": { "name": "USD Coin", "version": "2" }  // advertised — see the trap below
  }
}

Sign an EIP-3009 transferWithAuthorization

The agent signs a gasless USDC authorization for amountBaseUnits, to = payTo, under the USDC token's EIP-712 domain. The verifyingContract is the chain's USDC token contract (not your payTo), because that's what the facilitator recovers under:

NetworkchainIdverifyingContract (USDC)domain nameversion
Base mainnet84530x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"USD Coin""2"
Base Sepolia845320x036CbD53842c5426634e7929541eC2318f3dCF7e"USDC""2"
sign.ts (mirrors live/fire.ts)
import { privateKeyToAccount } from 'viem/accounts';
import { getAddress, toHex } from 'viem';
import { randomBytes } from 'node:crypto';

const account = privateKeyToAccount(process.env.BUYER_PRIVATE_KEY as `0x${string}`);
const now = Math.floor(Date.now() / 1000);

const domain = {
  name: 'USD Coin', version: '2', chainId: 8453,          // Base mainnet
  verifyingContract: getAddress('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'),
} as const;

const types = {
  TransferWithAuthorization: [
    { name: 'from', type: 'address' }, { name: 'to', type: 'address' },
    { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' },
    { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' },
  ],
} as const;

const message = {
  from: account.address,
  to: getAddress(payTo),                  // payTo from the payment_required
  value: BigInt(amountBaseUnits),         // exact base units from the quote — never a re-derived float
  validAfter: BigInt(now - 600),          // 10 min back for clock skew
  validBefore: BigInt(now + 300),
  nonce: toHex(randomBytes(32)) as `0x${string}`,  // random 32 bytes = idempotency key
};

const signature = await account.signTypedData({
  domain, types, primaryType: 'TransferWithAuthorization', message,
});

Trap — the domain name differs per chain.

Base mainnet USDC reports name: "USD Coin"; Base Sepolia reports name: "USDC". Sign under the name the live quote advertised (eip712Domain.name), falling back to the per-chain table above. The wrong name recovers to the wrong address and the transfer silently fails to authorize (SIGNER_MISMATCH). For Sepolia, swap in the Sepolia verifyingContract, chainId: 84532, and name: "USDC".

Settle → get the order + signed receipt

Submit the signed authorization. The control plane runs the SDK acceptance gate, then settles through the facilitator. The on-chain txHash is authoritative.

POST /m/<id>/agent/checkout/<session>/settle
curl -X POST https://merchants.veto-ai.com/m/mrch_01J…/agent/checkout/sess_…/settle \
  -H "Content-Type: application/json" \
  -d '{
    "payment": {
      "scheme": "x402-exact",
      "authorization": {
        "from": "0xBuyer…",
        "to": "0xYourReceivingAddress…",
        "value": "5000000",
        "validAfter": 1718900000,
        "validBefore": 1718900300,
        "nonce": "0x…32bytes…"
      },
      "signature": "0x…"
    }
  }'

On accepted you get back the order_id, a signed merchant receipt, and fulfillment (whose reference is the on-chain txHash for x402):

{
  "status": "accepted",
  "order_id": "…",
  "receipt": "<compact JWS merchant receipt>",
  "fulfillment": { "rail": "x402", "reference": "0x…txHash…" }
}

The receipt is an Ed25519-signed compact JWS anyone can verify offline against the merchant's public JWKS.

The settle confirms payment — it does not deliver the product.

There's no in-band product delivery here: the buyer gets what they paid for when your server fulfills it, triggered by the order.settled webhook (with the items + buyer's shipping address). See How Veto works → Deliver and Webhooks.

Test merchant vs live merchant.

A test merchant settles through the mock facilitator — the full gate runs and a real signed receipt is issued, but no funds move (you can even skip the live chain entirely). A live merchant moves real USDC on Base: the buyer's balance falls by exactly the quoted amount and the txHash confirms on BaseScan. This whole step mirrors live/fire.ts, the harness that fires one real ~$0.01 settlement through production.

8. Go live

When the sandbox flow is green end to end, flip three things together:

  1. Use your veto_live_ key (from Dashboard → Developers) — a live key is the only thing that reaches the url-mode x402-rs facilitator that moves real USDC.
  2. Create a live merchant (the live key makes it livemode: true automatically).
  3. Set receiving to your real Base mainnet USDC address (chain: "base").

Then run one small real purchase and confirm the txHash on BaseScan before you open the doors. The full cutover — the domain-name trap, the txHash-is-authoritative rule, and the go-live checklist — is in Go live with x402.

Prefer to have your agent do it?

You can hand all of steps 1–6 to your coding agent. The @veto-protocol/mcp-merchant MCP server exposes veto_create_merchant, veto_add_product, veto_set_receiving, veto_publish, and friends — so you describe your shop in plain English and the agent calls the same endpoints. See Set up Veto with your coding agent.