← Back to blog

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

There are two ways for an AI agent to buy a Steam key from us. The short one is a single gasless call. The agent signs an EIP-3009 authorization and sends it as an X-PAYMENT header on POST /purchase. A facilitator settles it on-chain, you pay no gas, and the key arrives in that same response. The long one is three explicit HTTP calls over the x402 protocol. The agent finds the product and calls POST /purchase, which answers with HTTP 402: the USDC amount, the payment address and a quote_id. It sends that USDC on Base, then calls POST /purchase again with the tx_hash and quote_id. Neither path needs an account, an API key or a human tapping confirm.

Think of this page as the reference. We start with the one-shot and then document the explicit two-step flow under it, which any agent can fall back on. That means exact request and response shapes, the real contract address, the chain ID, how many confirmations we wait for, and how the quote lock works. Looking for full agent code? The companion post covers building a game-buying agent in about 200 lines, and this post is the spec that code implements.

The fast path: one call

Any x402 client can sign an EIP-3009 transferWithAuthorization, and so can the cdkbot CLI. If your agent has one of those, the purchase shrinks to a single request. Take the accepts[] from the 402, sign an exact authorization for it, base64-encode that and send it as an X-PAYMENT header. A facilitator verifies and settles it on Base, gaslessly, and the key comes back in the response. You skip the second call and never have to track a tx_hash or quote_id.

# 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

The rest of this post describes the explicit two-step flow underneath the one-shot. It's the universal fallback, meant for an agent that prefers to broadcast the USDC transfer on its own.

The three calls at a glance

In the explicit flow there are three HTTP calls, with one onchain transaction between the second and third. (The one-shot merges calls 2 and 3.) Here's what goes out and what comes 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

The agent broadcasts a single ERC-20 USDC transfer on Base after call 2 and before call 3.

That table is the entire protocol. The sections below just fill in each row.

Step 1: Find the product

The agent starts with a human request, something like "buy Hollow Knight: Silksong on Steam", and has to turn it into a specific product ID. It calls GET /games/match with a title and platform. Back comes the best match along with that 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": [ ... ]
  }
}

Because the catalog holds more than 40,000 distinct SKUs across platforms and regions, picking the best option is a genuine filtering job. For browsing, GET /games takes structured parameters (platform, device, region, language). Each product's marketplace_offers array lists current prices from public price-comparison sources such as G2A, Eneba, Kinguin and others, which lets the agent judge whether a price is good without scraping anything. For the next step, though, it only needs the product id.

Step 2: POST /purchase, get the 402

Now the agent posts that product ID without paying. The server answers with HTTP 402, "Payment Required", and includes structured instructions for payment. This is the core idea of x402. The payment request rides in the HTTP response itself instead of sitting 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"
  }
}

These are the fields that matter:

  • amount + currency. How much USDC to send, exactly. The price is tied to the quote_id for 5 minutes and won't move between this call and confirmation. Wholesale prices do move, and the lock keeps either side from haggling afterwards.
  • chain + chain_id. Base mainnet, which is chain ID 8453. USDC there is an ERC-20 with 6 decimals.
  • asset. Base's native USDC contract, 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. If you send a different token, verification fails.
  • address. The recipient for the transfer.
  • quote_id. It locks the price and doubles as the anti-front-running token. Hold on to it, because call 3 won't work without it.

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

Paying is one ERC-20 transfer of the quoted USDC amount to the given address. Any wallet sends this kind of transaction, and no human approval popup is involved. It's one viem call in TypeScript or one web3 call in Python. Nothing about it is specific to CDK Bot; it's an ordinary onchain transfer.

When the transfer has been mined, the agent calls POST /purchase a second time. Now it includes the transaction hash and the quote ID it kept from step 2:

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"
}

The server checks the transaction directly on Base during this call. It confirms the token, the amount and the recipient address are all correct, then waits for 5 block confirmations, about 10 seconds on Base. Next it pulls the from address out of the Transfer event and binds it to the order permanently.

Most of the security model rests on that binding. Fetching the key, or asking for a refund later, only works from the wallet that paid. Suppose an attacker is watching the mempool and copies your tx_hash. They still can't claim the key without your quote_id, and they aren't the sending wallet anyway. Each tx_hash is also unique to one order, so nobody can replay it to get a second key.

What if 5 minutes pass before confirmation? The quote expires and the server responds with HTTP 410. The agent asks for a fresh quote and tries again.

A side note for x402 purists. In the canonical "exact" scheme, the client signs an EIP-3009 authorization and a facilitator settles it. In this two-step flow the agent broadcasts the transfer itself, and we verify it onchain. The HTTP-402 shape is the same and so is the result (no human involved), but strictly speaking it's an x402-style flow, not the literal signed-authorization variant.

The MCP alternative: no HTTP plumbing at all

Agents running inside an MCP-capable host such as Claude Desktop, Cursor or Cline don't need to touch raw HTTP. We wrapped the same flow in a Model Context Protocol server you reach through a zero-config URL. There's nothing to install and no API key:

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

The model then gets 8 tools it can call directly: search_games, get_game_details, browse_filters, get_price_quote, confirm_purchase, check_order, request_refund, and submit_review. They line up with the three calls above. Step 1 is search_games / get_game_details, the 402 in step 2 is get_price_quote, and step 3 is confirm_purchase. The wallet transfer still happens onchain. What MCP saves you is shaping the requests.

Recap: the whole thing in one breath

  • Find. GET /games/match returns the product id, along with competitor prices.
  • Quote. POST /purchase returns HTTP 402 with the amount, address and quote_id. The price stays locked for 5 minutes.
  • Pay. Send one USDC transfer on Base (chain 8453, asset 0x8335...2913).
  • Confirm. POST /purchase with tx_hash + quote_id. After 5 confirmations it returns the key_code.

On the autonomous path you never create an account, register a callback or wait for a human tap. That design is the reason this category works in the first place. We laid out the wider argument in why agents won't buy pizza first, and the working code lives in building a game-buying agent in 200 lines.

Build against it

Every field and status code is in the live OpenAPI spec at api.cdk.bot/docs. For programmatic discovery, other agents can read the agent card at /.well-known/agent.json. If you hit an edge case, want a new product category or find a flow we haven't documented, email 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.