Blog
    Integration8 min read

    Charge AI Agents per Call with x402 and Cred

    Let AI agents prepay for your API in stablecoin through Cred's x402 purchase endpoint, then deduct credits on every call with idempotency keys and a spending-safe retry policy.

    By cred.diy Team

    Charge AI Agents per Call with x402 and Cred

    You'll let AI agents prepay for your API in stablecoin over x402, then charge them a fixed number of credits on every call. Agents buy credits from Cred's public POST /x402/purchase endpoint without a Cred account, a card or an API key, and your API deducts from that balance on each call, using an idempotency key.

    So the agent pays once on-chain for a block of credits, and each call afterwards is a fast ledger write. That avoids an on-chain settlement on every request.

    Prerequisites

    • A cred.diy project, a credit type to sell, and an API key (sk_live_...) for your server.
    • Your project ID: Dashboard → Project Selector → hover the project → "ID".
    • x402 turned on in Dashboard → Agent Payments: a receiving wallet you control, a price per credit, the credit type for sale, and minimum and maximum purchase sizes. These settings decide where money lands, so they're only available in the dashboard, not over the API.
    • An agent wallet that can sign an EIP-3009 transferWithAuthorization (the x402 exact scheme on EVM).

    Network note: By default the deployment quotes USDC on Base Sepolia (eip155:84532), a testnet, through the public https://x402.org/facilitator. Test payments aren't revenue. Check what is actually quoted before you rely on it.

    How the flow works

    1. The agent calls your API with its account token. Your API deducts credits from Cred.
    2. When the balance runs out, your API answers 402 and tells the agent where to top up.
    3. The agent posts to /x402/purchase with no payment and gets a 402 with a PAYMENT-REQUIRED header (one of the three x402 v2 headers) (base64 JSON with the scheme, network, asset, amount and recipient).
    4. The agent signs those requirements and repeats the same request with a PAYMENT-SIGNATURE header.
    5. Cred verifies and settles through the facilitator, issues the credits, and returns a receipt plus a PAYMENT-RESPONSE header.
    6. The agent retries its original call to your API.

    Step 1: Confirm what Cred is quoting

    const res = await fetch(
      "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/x402/supported?project_id=your_project_id"
    );
    const support = await res.json();
    // support.project.accepts_x402        -> true once Agent Payments is enabled
    // support.project.price_per_credit_usd, min_credits, max_credits
    // support.project.purchase_url        -> .../credits-api/x402/purchase
    

    This endpoint is public, and it's the same one an agent uses to discover your price. If you override the settlement asset for your project, read the project's asset, not the platform's.

    Step 2: Decide how agents are identified

    When an agent buys, Cred credits a buyer_id. If the agent doesn't send one, the default is x402:<paying address>. Your API then has to know which buyer is calling. You have two options:

    • Accounts you issue (recommended). An agent registers with your service and gets your own token plus an account ID such as agt_42. It buys with buyer_id: "agt_42". Your API checks the token and deducts from agt_42.
    • Wallet identity. The agent omits buyer_id and is credited as x402:0x.... This only works if your API can prove, on every call, that the caller controls that address. That's a signature check you would have to build.

    buyer_id is whatever the payer sends. Anyone who pays can add credits to any buyer_id. That's harmless, since it only ever adds credits, but it means a balance existing for an ID proves nothing about who is calling. Your own authentication does that.

    Step 3: Charge per call in your API

    // A fetch-style handler (Next.js route handler, Hono, Workers...).
    const CRED_API = "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api";
    const COST_PER_CALL = 1;
    
    export async function POST(req: Request) {
      const account = await authenticateAgent(req); // your token -> { id: "agt_42" }
      if (!account) return Response.json({ error: "Unauthorized" }, { status: 401 });
    
      const callId = req.headers.get("Idempotency-Key");
      if (!callId) return Response.json({ error: "Idempotency-Key header required" }, { status: 400 });
    
      const res = await fetch(`${CRED_API}/deduct`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-api-key": process.env.CRED_API_KEY!,
          "Idempotency-Key": `call:${account.id}:${callId}`,
        },
        body: JSON.stringify({
          user_id: account.id,
          credit_type_id: process.env.CRED_CREDIT_TYPE_ID,
          amount: COST_PER_CALL,
          reason: "search call",
          reference_id: callId,
        }),
      });
      const charge = await res.json();
    
      // No balance yet (USER_NOT_FOUND) or not enough left: tell the agent where to buy.
      if (charge.code === "INSUFFICIENT_CREDITS" || charge.code === "USER_NOT_FOUND") {
        return Response.json(
          {
            error: "Out of credits",
            top_up: {
              purchase_url: `${CRED_API}/x402/purchase`,
              project_id: process.env.CRED_PROJECT_ID,
              buyer_id: account.id,
              cost_per_call: COST_PER_CALL,
            },
          },
          { status: 402 },
        );
      }
      if (!res.ok) return Response.json({ error: "Billing unavailable" }, { status: 503 });
    
      // Same Idempotency-Key as a call we already charged: don't serve it again for free.
      if (charge.idempotent_replay) {
        return Response.json({ error: "Call already processed" }, { status: 409 });
      }
    
      const result = await runSearch(await req.json());
      return Response.json({ result, credits_remaining: charge.new_balance });
    }
    

    The deduct is atomic. It returns HTTP 400 with code: "INSUFFICIENT_CREDITS" rather than going negative, so parallel agent calls can't overspend. If your work fails after the charge, return the credits with POST /refund and { "transaction_id": charge.transaction_id }.

    This 402 is not an x402 challenge for your endpoint. It points the agent at Cred's purchase endpoint, which issues the real challenge. Don't put a hand-built PAYMENT-REQUIRED header on your own response unless you implement verification and settlement for it.

    Step 4: The agent buys credits

    const PURCHASE = "https://eutdgemlrpvnxfkkpvlu.supabase.co/functions/v1/credits-api/x402/purchase";
    const order = { project_id: "your_project_id", credits: 500, buyer_id: "agt_42" };
    
    // 1. Ask with no payment attached: expect a 402 carrying the price.
    const quote = await fetch(PURCHASE, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(order),
    });
    if (quote.status !== 402) throw new Error(`Expected a quote, got ${quote.status}`);
    const required = JSON.parse(atob(quote.headers.get("PAYMENT-REQUIRED")!));
    const offer = required.accepts[0]; // { scheme, network, amount, asset, payTo, extra }
    
    // 2. Treat the quote as untrusted input. Check it against what you expect.
    if (
      offer.network !== EXPECTED_NETWORK ||
      offer.asset.toLowerCase() !== EXPECTED_ASSET ||
      offer.payTo.toLowerCase() !== EXPECTED_PAY_TO ||
      BigInt(offer.amount) > MAX_SPEND_ATOMIC
    ) {
      throw new Error("Quote outside this agent's spending policy");
    }
    
    // 3. Sign (EIP-3009 for USDC) and send the same request with the payload attached.
    const signedPaymentPayload = await wallet.signX402(offer); // your x402 client or wallet
    const paid = await fetch(PURCHASE, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": btoa(JSON.stringify(signedPaymentPayload)),
      },
      body: JSON.stringify(order),
    });
    const receipt = await paid.json();
    // { success: true, credits_purchased: 500, new_balance: 500, buyer_id: "agt_42",
    //   payer: "0x...", price_paid: 5, currency: "USDC", transaction: "0x...", x402_payment_id: "..." }
    

    amount is in the asset's atomic units as a string. With 6-decimal USDC, 500 credits at $0.01 is "5000000". credits has to fall between your project's minimum and maximum, or the request is refused with 400.

    Step 5: Handle retries and timeouts

    • Calls to your API: the agent sends the same Idempotency-Key when it retries a call. Cred replays the first deduct instead of charging again.
    • Purchases: a signed authorization can be used only once. If you present it again, you get 402 with "This payment authorization has already been used. Sign a new payment." So after a timeout, the agent shouldn't sign a new payment straight away. It should first check its balance through your API (for example a GET /me/credits route that calls Cred's GET /balance?user_id=agt_42). The first payment may well have gone through.
    • Your reconciliation: GET /x402/receipts?status=settled&limit=50 with your API key lists the payments taken for your project, with transaction hashes. x402 sales are recorded like card sales, with payment_provider set to "x402".

    Common mistakes

    • Treating buyer_id as authentication. It's chosen by the payer. Authenticate the agent with your own credential.
    • Signing whatever the 402 asks for. Check the network, asset, recipient and amount against a fixed policy, and enforce a per-run budget in the agent.
    • Re-signing after a timeout. Check the balance first, because the first payment may have settled.
    • Giving the agent your Cred API key. It doesn't need one. /x402/supported and /x402/purchase are public, and only your server deducts.
    • Shipping testnet defaults as production. The x402 facilitator docs say the x402.org facilitator is intended for development and testnet workflows.
    • Quoting a non-stablecoin asset. The price is credits × price per credit in the asset's units, with no exchange rate, which assumes one unit is one dollar.
    • New idempotency keys on retry. A retried call with a fresh key is a second charge.

    FAQ

    Does the agent need a Cred account? No. The purchase endpoints are public and the settled payment is the credential. Agents that don't send a buyer_id are identified by the wallet they paid from.

    Can an agent pay for Cred API calls directly? Yes, at the platform level. When your account's CRD runs out, any credits-api call answers 402 with payment requirements. Signing them and retrying the same request tops up CRD and runs the call.

    Is there an MCP version? The Cred MCP server exposes x402_supported and buy_credits_x402. Neither needs an API key. See MCP.

    Can humans buy the same credits? Yes. They can pay with a wallet on /pay/{project_id}, or by card through Stripe hosted checkout. All of these write to the same ledger.

    What does Cred charge? Each API call you make uses 1 CRD. See pricing.

    Sources

    Try it on a real project.