← Back to blog

How an AI agent buys a Steam key with USDC (step by step)

An AI agent can buy a Steam key in a single gasless call — or in three explicit HTTP calls over the x402 protocol. The one-shot: sign an EIP-3009 authorization and send it as an X-PAYMENT header on POST /purchase; a facilitator settles it on-chain (you don't pay gas) and the key comes back in the same response. The explicit version finds the product, then POST /purchase returns HTTP 402 with the USDC amount, the payment address, and a quote_id; the agent sends that USDC on Base, then calls POST /purchase a second time with the tx_hash and quote_id. Either way: no account, no API key, no human tapping confirm.

This is the step-by-step reference. It leads with the one-shot, then documents the explicit two-step flow underneath — the precise request and response shapes, the real contract address, the chain ID, the confirmation count, and the quote lock — which any agent can use as a fallback. If you want the full agent code instead, the companion post walks through building a game-buying agent in about 200 lines. This post is the spec it implements.

The fast path — one call

If your agent can sign an EIP-3009 transferWithAuthorization (any x402 client, or the cdkbot CLI), the whole purchase collapses to a single request: base64-encode a signed exact authorization for the accepts[] the 402 returns, send it as an X-PAYMENT header, and a facilitator verifies and settles it on Base — gaslessly — handing back the key in the response. No second call, no tx_hash, no quote_id to track.

# The cdkbot CLI signs and settles for you (gasless):
cdkbot buy <game_id> --key $PRIVATE_KEY

# Raw HTTP — one POST with the signed header:
POST /purchase   { "game_id": "a1b2c3..." }
X-PAYMENT: <base64 EIP-3009 exact authorization>
→ 200 OK + key_code

Everything below is the explicit two-step flow the one-shot is built on — the universal fallback for an agent that would rather broadcast the USDC transfer itself.

The three calls at a glance

The explicit flow is three HTTP calls and one onchain transaction between them (the one-shot above folds calls 2 and 3 into one). Here is what you send and what you get back at each step.

Call You send You get
1. Find GET /games/match?title=...&platform=Steam Best-match product, its id, and live competitor prices
2. Quote POST /purchase with { game_id } HTTP 402 + payment address, USDC amount, chain, and quote_id (locked 5 min)
3. Confirm POST /purchase with { game_id, tx_hash, quote_id } HTTP 200 + the key_code

Between call 2 and call 3 the agent broadcasts one ERC-20 USDC transfer on Base.

That is the whole protocol. Everything below is the detail behind each row.

Step 1 — Find the product

First the agent resolves a human request ("buy Hollow Knight: Silksong on Steam") to a specific product ID. Call GET /games/match with the title and platform and you get a best match back, plus the product's stable id:

GET /games/match?title=Hollow+Knight+Silksong&platform=Steam

{
  "best_match": {
    "id": "a1b2c3...",
    "name": "Hollow Knight: Silksong",
    "platform": "Steam",
    "region": "GLOBAL",
    "marketplace_offers": [ ... ]
  }
}

The catalog spans more than 40,000 distinct SKUs across platforms and regions, so "find the best option" is a real filter task — you can also browse with structured parameters via GET /games (platform, device, region, language). The marketplace_offers array carries current prices from public price-comparison sources (G2A, Eneba, Kinguin and others) so the agent can answer "is this a good price?" without scraping. The only thing the next step needs is the product id.

Step 2 — POST /purchase, get the 402

The agent posts the product ID with no payment, and the server replies with HTTP 402 — "Payment Required" — carrying structured instructions for how to pay. This is the heart of x402: payment lives inside the HTTP response, not behind a checkout form.

POST /purchase
{ "game_id": "a1b2c3..." }

HTTP/1.1 402 Payment Required
{
  "quote_id": "q_8f2a...",
  "payment": {
    "amount": "24.99",
    "currency": "USDC",
    "chain": "base",
    "chain_id": 8453,
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "address": "0x...recipient"
  }
}

The fields that matter:

  • amount + currency. The exact USDC to send. This price is locked to the quote_id for 5 minutes, so it can't drift between this call and confirmation. Wholesale prices move; the lock means neither side haggles after the fact.
  • chain + chain_id. Base mainnet, chain ID 8453. USDC is a 6-decimal ERC-20.
  • asset. The native USDC contract on Base: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. Send to the wrong token and verification will fail.
  • address. Where to send the transfer.
  • quote_id. The price lock and the anti-front-running token. Keep it — call 3 requires it.

Step 3 — Pay with a wallet (USDC on Base)

With a wallet, the agent simply broadcasts one ERC-20 transfer of the quoted USDC amount to the address — the same transaction any wallet sends, with no human approval popup. In TypeScript that's a single viem call; in Python it's one web3 call. There's nothing specific to CDK Bot about it — it's a standard onchain transfer.

Once the transfer is mined, the agent calls POST /purchase again, this time with the transaction hash and the quote ID it saved:

POST /purchase
{
  "game_id": "a1b2c3...",
  "tx_hash": "0xabc...",
  "quote_id": "q_8f2a..."
}

HTTP/1.1 200 OK
{
  "order_id": "ord_...",
  "key_code": "XXXXX-XXXXX-XXXXX",
  "status": "delivered"
}

On this call the server verifies the transaction directly on Base: it confirms the transfer is the right token, the right amount, to the right address, and waits for 5 block confirmations (roughly 10 seconds on Base). It then extracts the from address out of the Transfer event and permanently binds it to the order. That binding is the security model: only the wallet that paid can fetch the key or later request a refund. Even an attacker watching the mempool who copies your tx_hash can't claim the key, because they don't have your quote_id and they aren't the sending wallet. The tx_hash is also unique per order, so it can't be replayed for a second key.

If 5 minutes elapse before confirmation, the quote expires and the server answers HTTP 410 — the agent just requests a fresh quote and tries again. (One honest note for x402 purists: the canonical "exact" scheme has the client sign an EIP-3009 authorization that a facilitator settles. Here the agent broadcasts the transfer itself and we verify it onchain — same HTTP-402 shape, same no-human result, an x402-style flow rather than the literal signed-authorization variant.)

The MCP alternative — no HTTP plumbing at all

If the agent runs inside an MCP-capable host (Claude Desktop, Cursor, Cline), it can skip the raw HTTP entirely. The same flow is wrapped in a Model Context Protocol server with a zero-config URL — no install, no API key:

{
  "mcpServers": {
    "cdk": {
      "url": "https://mcp.cdk.bot/mcp",
      "transport": "streamable-http"
    }
  }
}

That exposes 8 tools the model can call directly: search_games, get_game_details, browse_filters, get_price_quote, confirm_purchase, check_order, request_refund, and submit_review. The mapping to the three calls above is direct: search_games / get_game_details cover step 1, get_price_quote is the 402 in step 2, and confirm_purchase is step 3. The wallet transfer still happens onchain; MCP just removes the request-shaping work.

Recap — the whole thing in one breath

  • Find. GET /games/match → product id (plus competitor prices).
  • Quote. POST /purchase → HTTP 402 with amount, address, and quote_id, price locked 5 minutes.
  • Pay. One USDC transfer on Base (chain 8453, asset 0x8335...2913).
  • Confirm. POST /purchase with tx_hash + quote_id → verified after 5 confirmations → key_code.

No accounts, no callbacks, no human tap on the autonomous path. That's the design point behind why this category works at all — we made the wider argument in why agents won't buy pizza first, and the working code in building a game-buying agent in 200 lines.

Build against it

The live OpenAPI spec with every field and status code is at api.cdk.bot/docs. Other agents can discover the service programmatically via the agent card at /.well-known/agent.json. Edge cases, new product categories, or a flow we haven't documented — info@cdk.bot.


Want to build on CDK Bot? Start with the cdk-agent-example repo or read the API docs. Questions: info@cdk.bot.