DEV Community

Edison Flores
Edison Flores

Posted on

Agent-to-agent payments: how x402 + AP2 mandates work in practice

AI agents need to buy things. Not in the future — today. An agent that finds a skill it needs should be able to pay for it and install it, without a human in the loop for every transaction.

This is how we implemented autonomous agent payments at MarketNow using x402 (HTTP 402 Payment Required) and AP2 (Agent Payments Protocol) delegated mandates.

The flow

Agent → POST /api/agent-purchase {skillId, mandateId}
       ← 402 Payment Required (x402 challenge)
Agent → sends USDC on Base
Agent → POST /api/agent-purchase {skillId, mandateId, txHash}
       ← 200 OK {license, system_prompt, install}
Enter fullscreen mode Exit fullscreen mode

Step 1: The 402 response

When an agent requests a paid skill without payment, we return HTTP 402:

res.setHeader('WWW-Authenticate', `x402 realm="marketnow", chain="base", token="USDC"`);
res.setHeader('X-Payment-Amount', String(skill.price * 10**6));
res.setHeader('X-Payment-Token', 'USDC');
res.setHeader('X-Payment-Chain', 'base');
res.setHeader('X-Payment-Contract', '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913');
res.setHeader('X-Payment-To', '0x39Dddf5aEdb58A559CF195fB8bdF23F0604Bf5Ee');
return res.status(402).json({
  x402: {
    accepts: {
      scheme: 'x402',
      network: 'base',
      asset: 'USDC',
      amount: skill.price,
      to: '0x39Dd...f5Ee',
    },
    retry_instructions: {
      method: 'POST',
      url: 'https://marketnow.site/api/agent-purchase',
      body: { skillId, mandateId, txHash: '<USDC tx hash on Base>' },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

The agent reads the headers, sends USDC, and retries with the txHash.

Step 2: Verify the payment on-chain

We verify the USDC transfer by calling eth_getTransactionReceipt on Base:

async function verifyUsdcTx(txHash, expectedAmountRaw, expectedFromWallet) {
  const { result: receipt } = await baseRpc.call(
    'eth_getTransactionReceipt', [txHash]
  );

  // Check tx succeeded
  if (receipt.status !== '0x1') return { ok: false, code: 'tx_failed' };

  // Parse Transfer event logs
  for (const log of receipt.logs) {
    if (log.address.toLowerCase() !== USDC_CONTRACT) continue;
    if (log.topics[0] !== TRANSFER_TOPIC) continue;

    const from = '0x' + log.topics[1].slice(26);
    const to = '0x' + log.topics[2].slice(26);
    const value = BigInt(log.data);

    if (to.toLowerCase() === PAYMENT_WALLET.toLowerCase()) {
      // C3 FIX: exact amount match (not >=)
      if (value !== BigInt(expectedAmountRaw)) {
        return { ok: false, code: 'amount_mismatch' };
      }
      // C4 FIX: validate sender wallet
      if (from.toLowerCase() !== expectedFromWallet.toLowerCase()) {
        return { ok: false, code: 'wrong_sender' };
      }
      return { ok: true, from, amount: Number(value) };
    }
  }
  return { ok: false, code: 'no_transfer_to_marketnow' };
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Replay defense

Each txHash can only be used once. We persist used txHashes as files in the GitHub repo:

async function isTxHashUsed(txHash) {
  const url = `https://raw.githubusercontent.com/${repo}/master/_data/used_txs/${txHash}.json`;
  const r = await fetch(url);
  return r.status === 200; // already used
}

async function markTxHashUsed(txHash, licenseKey, skillId, amount) {
  // Write file to GitHub via Contents API
  await fetch(`${GITHUB_API}/repos/${repo}/contents/_data/used_txs/${txHash}.json`, {
    method: 'PUT',
    headers: { Authorization: `Bearer ${token}` },
    body: JSON.stringify({
      message: `tx: mark used ${txHash}`,
      content: Buffer.from(JSON.stringify({ txHash, skillId, licenseKey, amount })).toString('base64'),
    }),
  });
}
Enter fullscreen mode Exit fullscreen mode

AP2 Mandates: human-in-the-loop by default

Before an agent can buy, the human principal creates a mandate with spending limits:

{
  "owner": "0xABC...",
  "agentId": "claude-001",
  "spendingLimitUsd": 50,
  "perPurchaseCapUsd": 10,
  "notificationMode": "notify",
  "expiresAt": "2026-10-01T00:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Key design decisions:

  1. Default is "notify" — the principal gets an email/webhook on every purchase. NOT silent.
  2. "silent" mode requires explicit confirmSilentAutonomy: true — opt-in, not default.
  3. Hard caps: $500 max total, $50 max per purchase, 90-day expiry.
  4. Instant revocation — principal can revoke anytime.
  5. Every spend is a git commit — full audit trail.

The security fixes we learned from a pentest

We had a security auditor review the implementation. They found 4 critical issues:

Issue Severity Fix
Mandate spend failed silently (cap never enforced) CRITICAL Fail-closed: if spend fails, NO license issued
txHash replay (same payment used twice) CRITICAL Dedup store via GitHub _data/used_txs/
Amount mismatch (>= instead of ==) CRITICAL Exact match: value !== BigInt(expected)
Stolen txHash (anyone could redeem) CRITICAL Validate from wallet matches caller

All 4 are now fixed and live in production.

Try it

# Search for skills
curl "https://marketnow.site/api/search?q=scraper&limit=5"

# Get a free skill (no payment needed)
curl -X POST "https://marketnow.site/api/agent-purchase"   -H "Content-Type: application/json"   -d '{"skillId":"mn-prompt-63712dff"}'

# Buy a paid skill with USDC on Base
curl -X POST "https://marketnow.site/api/agent-purchase"   -H "Content-Type: application/json"   -d '{"skillId":"mn-gen-00001","mandateId":"mand_xxx","txHash":"0x...","walletAddress":"0x..."}'
Enter fullscreen mode Exit fullscreen mode

Links


MarketNow — trust layer for agent commerce. 8,560 MCP skills, Sentinel L2 security, x402 + USDC on Base. AliceLabs LLC (Wyoming, USA).

Top comments (0)