Hermes Plant logo

Hermes Plant

Pay-per-call finance APIs for AI agents

Open navigation
6 min read

x402 from curl: the smallest possible agent transaction

An x402-paid API call is two HTTP requests and one signature. The first request gets a 402 Payment Required back with a structured challenge. The agent signs the challenge with its wallet and replays the request with an X-PAYMENT header. The server settles on-chain and returns the actual response. This post walks through it with curl against a live Hermes Plant endpoint — no SDKs, no abstractions, no MCP layer.

Why curl first

Every x402 client library — the Coinbase one, ours, the Python ones starting to appear — is a thin shell over two HTTP calls. Reading them is faster than learning their API surfaces. And once you've made an unwrapped paid call from a terminal, the SDK becomes obvious instead of magical.

The endpoint we'll target is /agent-services/walletguard at $0.05 per call. It's deterministic (same input, same output), priced low enough that the math is real but the cost is trivial, and live on Base mainnet today.

Step 1: the unpaid request

Hit the endpoint without a payment header. The server responds with HTTP 402 and a structured challenge describing exactly what the agent must sign.

curl -i -X POST https://hermesplant.com/agent-services/walletguard \
  -H 'content-type: application/json' \
  -d '{"address":"0x0000000000000000000000000000000000000000","chain":"base"}'
Initial call — no payment header.

The response body is the x402 challenge. The shape is fixed by the x402 spec; the values are populated from this endpoint's pricing config.

HTTP/1.1 402 Payment Required
content-type: application/json

{
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "50000",
      "resource": "https://hermesplant.com/agent-services/walletguard",
      "description": "Hermes Plant — WalletGuard",
      "mimeType": "application/json",
      "payTo": "0x5171...4c1B",
      "maxTimeoutSeconds": 60,
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "extra": { "name": "USDC", "version": "2" }
    }
  ],
  "error": "Payment required"
}
402 challenge — agent reads accepts[0] to learn what to sign.

Three values matter here. asset is the USDC contract on Base mainnet. payTo is the merchant address that will receive the 50,000 base units (= $0.05 USDC). maxTimeoutSeconds bounds how stale the signed authorization can be before the facilitator rejects it.

Step 2: sign the authorization

x402 uses EIP-712 typed-data signatures over USDC's TransferWithAuthorization. The agent constructs the typed-data payload from the challenge, signs it with its wallet's private key, and base64-encodes the result into the X-PAYMENT header.

Here is the minimum viable signer in JavaScript. In production you'd use @x402/core or viem's signTypedData directly; this is what those wrappers do underneath.

import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY);
const accept = challenge.accepts[0];
const nonce = "0x" + crypto.getRandomValues(new Uint8Array(32))
  .reduce((s, b) => s + b.toString(16).padStart(2, "0"), "");
const validAfter = Math.floor(Date.now() / 1000) - 10;
const validBefore = validAfter + accept.maxTimeoutSeconds;

const signature = await account.signTypedData({
  domain: {
    name: accept.extra.name,
    version: accept.extra.version,
    chainId: 8453,                  // Base mainnet
    verifyingContract: accept.asset,
  },
  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" },
    ],
  },
  primaryType: "TransferWithAuthorization",
  message: {
    from: account.address,
    to: accept.payTo,
    value: BigInt(accept.maxAmountRequired),
    validAfter: BigInt(validAfter),
    validBefore: BigInt(validBefore),
    nonce,
  },
});

const xPayment = Buffer.from(
  JSON.stringify({
    x402Version: 1,
    scheme: "exact",
    network: "base",
    payload: {
      authorization: {
        from: account.address,
        to: accept.payTo,
        value: accept.maxAmountRequired,
        validAfter: String(validAfter),
        validBefore: String(validBefore),
        nonce,
      },
      signature,
    },
  }),
).toString("base64");
Sign the EIP-712 authorization and pack it into X-PAYMENT.

Step 3: replay with the payment

Send the exact same request, this time with the X-PAYMENT header.

curl -i -X POST https://hermesplant.com/agent-services/walletguard \
  -H 'content-type: application/json' \
  -H "x-payment: $XPAYMENT" \
  -d '{"address":"0x0000000000000000000000000000000000000000","chain":"base"}'
Same call, now with the signed authorization.

The server validates the signature, hands the authorization to the facilitator for settlement, and (when settlement succeeds) executes the underlying logic and returns the response. An X-PAYMENT-RESPONSE header echoes the on-chain transaction hash, which is what x402scan watches.

HTTP/1.1 200 OK
content-type: application/json
x-payment-response: eyJzdWNjZXNzIjp0cnVlLCJ0cmFuc2FjdGlvbiI6IjB4...

{
  "address": "0x0000000000000000000000000000000000000000",
  "chain": "base",
  "risk": "high",
  "findings": [
    { "code": "ZERO_ADDRESS", "severity": "block",
      "detail": "Caller passed the canonical burn address." }
  ],
  "evidenceId": "ev_2026Q2_walletguard_018f..."
}
Paid response — deterministic findings plus an evidence id the agent can replay later.

What makes this useful for agents (not humans)

Three properties are doing the work here, and all three exist because the protocol — not the SDK, not us — guarantees them:

  • No account, no API key, no signup. The agent's wallet address is its identity. New agents transact in the same minute they're spawned.
  • Pay-per-call, settled per-call. There is no invoice, no monthly cap, no quota to busy-wait against. Cost is a function of usage; usage is a function of the agent's autonomous decisions.
  • On-chain settlement is the receipt. Hermes Plant's /evidence endpoint can replay any paid call by its transaction hash, which means the agent's run log is permanently auditable without us storing a session.

The whole flow, end-to-end

  1. Agent POSTs to the endpoint. Server returns 402 with a structured challenge.
  2. Agent signs an EIP-712 TransferWithAuthorization message from the challenge.
  3. Agent base64-encodes the authorization into X-PAYMENT and replays the request.
  4. Server validates the signature locally, then asks the facilitator to settle.
  5. Facilitator submits the USDC transfer on-chain. Server gates the response on settlement success.
  6. Server returns the real response with X-PAYMENT-RESPONSE containing the tx hash.

Two HTTP requests. One signature. Roughly 90 lines of JavaScript with no dependencies beyond viem. The Hermes Plant SDK and the Coinbase x402 SDK both wrap this; reading them now will be much faster than it would have been five minutes ago.

Try it yourself

  • Endpoint: https://hermesplant.com/agent-services/walletguard ($0.05/call)
  • x402 manifest: https://hermesplant.com/.well-known/x402
  • Reference impl: https://github.com/JesseGdotIO/hermesplant-examples (curl, TypeScript, Python, CrewAI, LangChain, MCP)
  • Full endpoint catalog: https://hermesplant.com/agent-services

If your agent makes a call and the response surprises you, mail contact@hermesplant.com with the transaction hash — every paid call has a replayable evidence id and we will tell you exactly what the engine saw.

Topics

  • x402
  • agent-payments
  • curl
  • deterministic-finance