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_ key | veto_live_ key | |
|---|---|---|
| Creates | a test merchant (livemode: false) | a live merchant (livemode: true) |
| Settles via | the mock facilitator — structural checks, no real funds | the url-mode x402-rs facilitator — real USDC on Base |
| Receiving chain | base-sepolia | base |
| Use it to | build and rehearse the full flow safely | take 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:
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.
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.
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:
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:
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.
# 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…"{ "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:
curl https://merchants.veto-ai.com/m/mrch_01J…/.well-known/agentic-checkout.jsonTo 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/catalogPick 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'.
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:
| Network | chainId | verifyingContract (USDC) | domain name | version |
|---|---|---|---|---|
| Base mainnet | 8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | "USD Coin" | "2" |
| Base Sepolia | 84532 | 0x036CbD53842c5426634e7929541eC2318f3dCF7e | "USDC" | "2" |
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.
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:
- 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. - Create a live merchant (the live key makes it
livemode: trueautomatically). - Set
receivingto 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.
How Veto works — discover, pay, deliver
The full arc, including how the buyer gets what they paid for (the webhook → your delivery).
Connect your site
Point a CNAME so the discovery anchor resolves under your own brand.
Go live with x402
The real-money cutover — live key, live merchant, your real address, and the traps.
Set up with your agent (MCP)
Tell your coding agent to stand the whole thing up for you.
Verify receipts
Check a merchant receipt offline against its public JWKS.
How Veto works — discover, pay, deliver
The whole model in plain English. An agent discovers your checkout, pays you USDC over x402, and then YOU deliver the goods — triggered by a signed order.settled webhook that carries what was bought, the amount, the receipt, and the buyer's shipping details.
Quickstart
Go from npx to an AI agent completing a sandbox checkout in about five minutes — one scaffold command, three run commands, four scenarios, zero install.