# Base USDC Payment Verifier (x402 API + MCP) — v1.3.0

Pay-per-call API that answers one question with evidence: **"Did transaction `txHash` deliver native USDC to `expectedTo` for `expectedAmount` on Base, and is it final enough to trust?"**

Category: **Crypto / payment verification**. Price: **0.01 USDC per call** via x402 v2 (`exact`, EIP-3009, native USDC). Payment network is set by the operator: the code defaults to Base Sepolia testnet (`eip155:84532`) with the public x402.org facilitator; production sets `X402_NETWORK=base` (Base mainnet `eip155:8453`, USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) with the Coinbase CDP facilitator. Check `GET /pricing` or `GET /health` for the live network.

## Who it is for
- Agents and merchants that accept USDC outside a facilitator (a direct wallet transfer, an invoice, a "paid — here is my tx hash" claim) and must confirm it before delivering work.
- Escrow / bounty / marketplace bots that release work on proof of payment.
- Auditors reconciling claimed x402 settlements (`PAYMENT-RESPONSE` tx hashes) against chain data.

A plain RPC call is not enough: you must pick the right receipt log, ignore look-alike "USDC" tokens, compare atomic amounts, check revert status, check the block is still canonical, count confirmations and reject stale (replayed) payments. This API does all of that and returns a single verdict with machine-readable reasons.

## Endpoint
`GET /verify?…` (query parameters, primary/discovery method) — or `POST /verify` with the same fields as a JSON body. Machine-readable contract: `GET /openapi.json` (OpenAPI 3.1 with `x-payment-info` and the `402` response); resource list: `GET /.well-known/x402`.

| field | required | notes |
|---|---|---|
| `txHash` | yes | 0x + 64 hex |
| `expectedTo` | yes | recipient address (case-insensitive) |
| `expectedAmount` *or* `expectedAmountAtomic` | yes | `"0.10"` (≤6 decimals) or `"100000"` (1 USDC = 1 000 000) |
| `chain` | no | `base` (default) or `base-sepolia` — chain of the tx being verified (independent of the payment network) |
| `amountMatch` | no | `exact` (default) or `atLeast` |
| `expectedFrom` | no | require this payer |
| `minConfirmations` | no | default 3, 1–10000 |
| `notBefore` | no | unix seconds; reject txs mined earlier (anti-replay of an old payment) |
| `maxAgeSeconds` | no | reject txs older than this |

Unknown fields are rejected (400) — there is no caller-supplied RPC URL.

### Response (HTTP 200)
- `valid` (bool), `verdict`: `valid` | `invalid` | `pending` | `not_found`
- `reasons[]` (empty when valid): `tx_not_found`, `tx_not_mined`, `tx_reverted`, `no_usdc_transfer`, `recipient_mismatch`, `sender_mismatch`, `amount_mismatch`, `amount_below_expected`, `insufficient_confirmations`, `block_not_canonical`, `wrong_chain_id`, `tx_before_not_before`, `tx_too_old`, `block_timestamp_unavailable`
- `warnings[]`: `non_usdc_token_transfer_to_recipient` (look-alike token spoof), `multiple_usdc_transfers_to_recipient`
- Evidence: `status`, `blockNumber`, `blockHash`, `blockTime`, `confirmations`, `latestBlock`, `txFrom`, `txTo`, `viaEip3009Authorization`, `matchedTransfer {logIndex, from, to, amountAtomic, amount}`, `totalUsdcToRecipient`, `usdcTransfers[]` (≤50), `lookalikeTokenTransfers[]`, `usdcContract`, `explorerUrl`, `checkedAt`.

Acceptance criteria: `valid=true` only if the receipt status is success, the block hash equals the canonical block at that height, a Transfer log **emitted by the canonical USDC contract** (Base `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`, Base Sepolia `0x036CbD53842c5426634e7929541eC2318f3dCF7e`) moves the expected amount to `expectedTo` (and from `expectedFrom` if given), time windows pass, and confirmations ≥ `minConfirmations`.

### Worked example (Base mainnet; replace the placeholders)
```bash
curl -s "https://HOST/verify?chain=base&txHash=<TX_HASH>&expectedTo=<YOUR_ADDRESS>&expectedAmount=0.10"
# or: curl -s -X POST https://HOST/verify -H 'content-type: application/json' \
#       -d '{"chain":"base","txHash":"<TX_HASH>","expectedTo":"<YOUR_ADDRESS>","expectedAmount":"0.10"}'
# unpaid -> HTTP 402 + PAYMENT-REQUIRED header (x402 v2). An x402 client signs and retries:
# -> {"valid":true,"verdict":"valid","reasons":[],"confirmations":…,"viaEip3009Authorization":true,
#     "matchedTransfer":{"logIndex":0,"from":"<PAYER>","to":"<your address>","amountAtomic":"100000","amount":"0.1"}, …}
```
Same tx with `expectedAmount=0.11` → `verdict:"invalid", reasons:["amount_mismatch"]`.

## MCP (Streamable HTTP)
`POST /mcp` exposes the same check as the paid MCP tool `verify_usdc_payment` (same price) plus the free `get_pricing` tool, using the x402 MCP transport: an unpaid call returns `isError: true` with `PaymentRequired` in `structuredContent`; retry with the payment in `params._meta["x402/payment"]`; the receipt comes back in `result._meta["x402/payment-response"]`. Details: `GET /mcp.md`. Registry metadata: `GET /server.json`.

## Errors (never charged)
| HTTP | error | meaning |
|---|---|---|
| 400 | `invalid_*`, `missing_amount`, `unknown_field`, `invalid_json` | bad input — rejected *before* payment is requested |
| 402 | — | payment required (x402 v2 challenge) |
| 413 | `body_too_large` | body > 4 KB |
| 502 | `rpc_unavailable` | public RPC down; not settled, retry later |
x402 settlement only happens on HTTP < 400.

## Limitations
- Only native Circle USDC on Base mainnet / Base Sepolia. Bridged USDbC, other tokens and other chains are out of scope (they appear only as look-alike warnings).
- Reads public RPC (`mainnet.base.org`, `sepolia.base.org`, publicnode fallback). Results reflect node state at `checkedAt`.
- Matches a single Transfer log; it does not sum several partial payments (see `totalUsdcToRecipient`).
- Not sanctions screening, identity or compliance. Read-only: never needs keys.

## Recovery
Verification is a deterministic read: re-sending the identical input recomputes the same answer (only `confirmations`/`latestBlock` grow). Through a gateway (e.g. Speedbot), use the gateway's recovery with your original payment authorization; never sign a second payment for a pending call.

## Run locally
```bash
npm install
cp .env.example .env     # defaults: Base Sepolia testnet (eip155:84532) + x402.org facilitator, 127.0.0.1:4031, payTo 0x64eB…3169, $0.01
npm start
npm test                 # unit (offline) + server/live-RPC + MCP tests (server must be running; set EXPECT_NETWORK=eip155:84532 for testnet)
# Mainnet: X402_NETWORK=base plus CDP_API_KEY_ID / CDP_API_KEY_SECRET in the environment (never in files)
```
Free routes: `/health`, `/pricing`, `/llms.txt`, `/` (summary), `/docs` + `/docs.md` (this file), `/mcp.md`, `/terms`, `/openapi.json`, `/.well-known/x402`, `/server.json`.

Terms: see `TERMS.md` (`GET /terms`).

## Free: bounty payer pre-check (v1.3.0)
`GET /bounty-check?address=0x…&amount=25&chain=base` (chain: `base` default, or `base-sepolia`). Free, no payment.

Returns neutral, point-in-time facts read from **public JSON-RPC only**:
- `account.type`: `eoa`, `contract`, or `eoa_delegated_7702` (EIP-7702 delegation designator) + code size
- `balances`: USDC (native Circle USDC) and ETH balance at `as_of.block`
- `activity.outgoing_tx_count` (account nonce), `activity.first_activity` (first block with nonce > 0 for EOAs, or with code for contracts — binary search on archive state; `null` with a note if unavailable or the address never sent a tx), `age_days`
- `recent_usdc_outflows`: USDC `Transfer(from = address)` logs over the last 24 h (count, total, largest, last 1 h, last outflow block / approximate time)
- `requested` and `assessment.balance_covers_amount` / `shortfall_usdc` — compares the **current USDC balance only** with the amount

Disclaimer (also in every response): Point-in-time snapshot of public on-chain data. Not a guarantee or prediction that any payment will be made; balances can change at any moment. No assessment of any person, organization or platform. No names, labels or identity data are used or returned. Not financial advice.

No ENS names, explorer labels/tags or identity inference are used. Rate limit: 6/min per IP and 60/min overall (`BOUNTY_RATE_PER_IP_PER_MIN`, `BOUNTY_RATE_GLOBAL_PER_MIN`); results cached 30 s per address. Upstream: `BASE_RPC_URLS` (balances), `BASE_LOGS_RPC_URLS` (default publicnode, then mainnet.base.org), `BASE_ARCHIVE_RPC_URLS` (default mainnet.base.org). Public RPCs are rate-limited, so outflows / first activity can come back `null` with a note.
