> For the complete documentation index, see [llms.txt](https://docs.madhousewallet.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.madhousewallet.com/quickstart/widget.md).

# x402 Agentic Payments

Call Madhouse's compliance, payout, and KYC APIs from an AI agent — paying per request in USDC with no API keys, via x402 and the AgentCash MCP.

Madhouse exposes three production APIs that an autonomous agent can call and pay for **per request**, in USDC, with **no API key, no signup, and no invoice**. Each endpoint is gated by the [x402 protocol](https://x402.org): the first request returns `402 Payment Required` with payment terms, the client signs a USDC micropayment, retries, and gets the data.

You don't implement x402 by hand. The [**AgentCash**](https://www.agentcash.dev) MCP server holds a USDC wallet and handles the 402 → sign → settle → retry loop for you. From the agent's side, it's a single `fetch` call.

| Service     | Base URL                  | What it does                                                           |
| ----------- | ------------------------- | ---------------------------------------------------------------------- |
| **Scanner** | `scan.madhousewallet.com` | Wallet risk scoring + sanctions name screening                         |
| **Payouts** | `try.madhousewallet.com`  | Quote and send cross-border fiat payouts (80+ currencies)              |
| **KYC**     | `kyc.madhousewallet.com`  | On-chain identity attestation (EAS / SAS) + wallet sanctions screening |

All three accept USDC on **Base, Polygon, Arbitrum** (EVM) and **Solana**. Payment settles through the Coinbase CDP facilitator, which sponsors gas — you only pay the listed price.

***

## How payment works

```
Agent ──fetch──▶ endpoint
        ◀── 402 Payment Required (price, network, payTo, asset)
AgentCash signs an EIP-3009 / SPL USDC transfer for the exact amount
Agent ──fetch (X-PAYMENT header)──▶ endpoint
        ◀── 200 OK + JSON body
```

There are no refunds — the `exact` scheme moves funds on-chain before the handler runs. Endpoints that mutate state (transfers, KYC sessions) are idempotent upstream, so a retry after a network blip does not double-charge the business logic, but **each HTTP attempt that reaches a 402 and pays is a separate USDC charge**. Cache successful responses; don't poll paid endpoints in a tight loop.

***

## Using AgentCash (the fast path)

AgentCash is already wired into this environment as an MCP server. The workflow is three tool calls:

**1. Check / fund the wallet.** Paid calls need a balance; the free endpoints don't.

```
get_balance()                 → { "usdc": "4.92", ... }
list_accounts()               → deposit addresses + funding link (if balance is low)
```

**2. Discover the endpoints for a base URL.** This pulls the live OpenAPI + price for every route — you don't hardcode paths.

```
discover_api_endpoints("https://scan.madhousewallet.com")
discover_api_endpoints("https://try.madhousewallet.com")
discover_api_endpoints("https://kyc.madhousewallet.com")
```

**3. Call the endpoint.** `fetch` handles the 402 automatically and returns the final body.

```
fetch({
  url: "https://scan.madhousewallet.com/api/public/quick-scan?address=vitalik.eth",
  method: "GET"
})
```

Pick the network with `fetch`'s payment options if you want to pay on Polygon/Arbitrum/Solana instead of Base — otherwise it defaults to the first accepted network (Base). Use `check_endpoint_schema` before a POST if you're unsure of the body shape.

> If you're not running AgentCash, any x402 client works — see [Calling raw x402](#calling-raw-x402) at the bottom.

***

## Pricing

Prices are fixed per call, charged in USDC regardless of which chain you pay on. The `/solana` variant of any endpoint is the same price as its EVM sibling — it just settles on Solana.

### `scan.madhousewallet.com`

| Endpoint                                                                   | Method | Price         | Notes                                                      |
| -------------------------------------------------------------------------- | ------ | ------------- | ---------------------------------------------------------- |
| `/api/public/quick-scan`                                                   | GET    | **$0.01**     | \~350 ms. Risk score + traits only.                        |
| `/api/public/scan`                                                         | GET    | **$0.01**     | \~9 s. Full metrics incl. USD volumes, ENS, contract flag. |
| `/api/public/sanctions-check`                                              | GET    | **$0.04**     | OpenSanctions name/entity screening.                       |
| `/api/public/quick-scan/solana`, `/scan/solana`, `/sanctions-check/solana` | GET    | same as above | Pay on Solana.                                             |

### `try.madhousewallet.com`

| Endpoint                           | Method | Price     |
| ---------------------------------- | ------ | --------- |
| `/api/public/quote`                | GET    | **$0.01** |
| `/api/public/requirements`         | GET    | **$0.01** |
| `/api/public/requirements/refresh` | POST   | **$0.01** |
| `/api/public/recipients`           | POST   | **$0.01** |
| `/api/public/transfer`             | POST   | **$0.01** |
| `/api/public/confirm-transfer`     | POST   | **$0.01** |
| `/api/public/transfer-status`      | GET    | **$0.01** |

> A complete payout costs \~$0.05–0.06 in x402 fees (quote → requirements → recipient → transfer → confirm), separate from the FX/payout fees on the money itself.

### `kyc.madhousewallet.com`

| Endpoint                    | Method | Price                                           |
| --------------------------- | ------ | ----------------------------------------------- |
| `/api/public/kyc/start`     | POST   | **$0.05**                                       |
| `/api/public/kyc/status`    | GET    | **$0.01**                                       |
| `/api/public/kyc/sanctions` | GET    | **$0.02**                                       |
| `/api/public/kyc/verified`  | GET    | **Free** (60 req/min/IP)                        |
| `/api/public/kyc/start-eoa` | POST   | **Free** (wallet-signature gated, 5 req/min/IP) |

Each paid endpoint has a `/solana` twin at the same price.

***

## Scanner — wallet risk & sanctions

`scan.madhousewallet.com`

### Quick scan (cheapest, real-time)

Best for pre-transaction screening. Accepts a 0x address or an ENS name.

**Request**

```
GET /api/public/quick-scan?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
```

**Response — flagged**

```json
{
  "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "toxicScore": 0.95,
  "riskLevel": "CRITICAL",
  "shouldFlag": true,
  "traits": [
    {
      "name": "initiator_scam_transactions",
      "risk": 0.95,
      "txsCount": 1,
      "description": "The address has initiated scam airdrops or poisoning attacks."
    }
  ]
}
```

**Response — clean**

```json
{ "address": "0x1234…", "toxicScore": 0, "riskLevel": "PASS", "shouldFlag": false, "traits": [] }
```

`riskLevel`: `PASS` (0) · `LOW` (0–0.40) · `MIDDLE` (0.41–0.80) · `CRITICAL` (≥0.81).

### Full scan

Same input, slower (\~9 s), richer output: `flaggedMetrics[]` carries `type` (`absolute` | `volume`), `incomingMoneyUSD`, `outgoingMoneyUSD`, plus `ens`, `isContract`, `addressExists`.

```json
{
  "address": "0xd8dA…", "toxicScore": 0.04, "riskLevel": "LOW", "shouldFlag": true,
  "flaggedMetrics": [
    { "name": "non_kyc_transfers", "risk": 0.03, "type": "volume",
      "description": "Transacted with non-KYC exchanges",
      "txsCount": 344, "incomingMoneyUSD": 15722.47, "outgoingMoneyUSD": 2484.15 }
  ],
  "ens": "vitalik.eth", "isContract": false, "addressExists": true
}
```

### Sanctions name check ($0.04)

Screens a person/company name against OpenSanctions (EU, UN + 330 lists).

**Request**

```
GET /api/public/sanctions-check?name=Viktor%20Bout&country=ru&birthDate=1967-01-13
```

`name` is required; `country` (ISO-2), `address`, and `birthDate` (`YYYY` or `YYYY-MM-DD`) are optional filters that cut false positives.

**Response**

```json
{
  "sanctioned": true,
  "name": "Viktor Bout",
  "matchCount": 1,
  "matches": [
    {
      "entityId": "Q314650",
      "caption": "Viktor Anatolijevitch BOUT",
      "entityType": "Person",
      "topics": ["role.pep", "sanction", "debarment"],
      "matchedOn": ["name", "country", "birthDate"],
      "datasets": ["us_ofac_sdn", "us_trade_csl", "eu_financial_sanctions_files"]
    }
  ]
}
```

**Edge cases**

* **Score normalization.** `toxicScore` is 0–1, but the upstream may return 0–100 for confirmed bad actors. The API normalizes (`s > 1 ? s/100 : s`) — trust the returned 0–1 value.
* **Off-ramp rule:** only `toxicScore === 0` is truly clean. `shouldFlag` is `true` for any non-zero score.
* **Sparse sanctions matching is AND-logic with fallback:** a name-only query (e.g. `John Smith`) returns many matches because `country`/`birthDate` filters are skipped when absent. Read `matchedOn[]` per match to judge confidence — a hit on `["name","country","birthDate"]` is high-confidence; `["name"]` alone is weak.
* `503` from sanctions = MongoDB blip; retry with backoff. `400` = missing `name` or bad address format.

***

## Payouts — cross-border fiat

`try.madhousewallet.com`

KYC and sanctions screening on the funding wallet and recipient are enforced upstream, so these endpoints are safe to drive from an agent. The flow is six paid calls:

```
1. GET  /api/public/quote                 → quoteId + FX rate + fees
2. GET  /api/public/requirements?currency=NGN   → dynamic bank-detail fields
3. POST /api/public/requirements/refresh  → (only if a field has refreshRequirementsOnChange)
4. POST /api/public/recipients            → recipientId
5. POST /api/public/transfer              → transfer_id + deposit_address
   (user sends USDC to deposit_address on Base)
6. POST /api/public/confirm-transfer      → status: processing
   GET  /api/public/transfer-status?id=…  → poll to completed
```

**Quote**

```
GET /api/public/quote?targetCurrency=NGN&sourceAmount=100
```

```json
{
  "quoteId": "7f3c1a2b-0d9e-4a6f-8c11-2b9d4e5f6a7b",
  "sourceAmount": 100,
  "targetCurrency": "NGN",
  "usdToTargetRate": 1580.42,
  "serviceFeePercent": 1.5,
  "quote": {
    "targetAmount": 155661.4, "transferFee": 0.5,
    "feeFxPercent": 0.47, "feeFxAmount": 0.32, "feePayoutAmount": 0.18,
    "estimatedDelivery": "2026-06-01T12:00:00.000Z"
  }
}
```

**Requirements** return field descriptors that vary per currency (Nigeria needs `bankCode` + 10-digit `accountNumber`; an IBAN country needs `IBAN`, etc.). If any field has `refreshRequirementsOnChange: true`, call `requirements/refresh` with the values chosen so far to reveal the conditional fields.

**Create recipient → transfer**

```
POST /api/public/recipients
{ "currency":"NGN", "type":"nigeria_bank_account", "accountHolderName":"Ada Lovelace",
  "details":{ "bankCode":"058", "accountNumber":"0123456789" },
  "wallet":"0x1234…" }
→ { "id": 12345, "active": true, ... }

POST /api/public/transfer
{ "quote_id":"7f3c…", "amount":100, "recipientId":12345,
  "customer_uuid":"2b9d…", "customer_email":"sender@example.com",
  "source_token":"usdc", "source_network":"base", "wallet_address":"0x1234…" }
→ { "transfer_id":"6650f1a2b3c4d5e6f7a8b9c0",
    "deposit_address":"0x…", "amount":100, "currency":"NGN",
    "instructions": { "send_amount":100, "send_token":"usdc", "send_network":"base", ... } }
```

After the user sends USDC to `deposit_address`, confirm with the on-chain tx hash:

```
POST /api/public/confirm-transfer
{ "transfer_id":"6650f1a2b3c4d5e6f7a8b9c0", "tx_hash":"0xabc…def" }
→ { "transfer_id":"6650…", "status":"processing", "message":"Transfer confirmed and queued for payout." }
```

`transfer-status` cycles through `pending → awaiting_deposit → deposit_sent → processing → completed` (or `failed`).

**Edge cases**

* **Quotes expire (\~5 min).** A stale `quote_id` fails at `/transfer` — re-quote and retry.
* **`amount` must equal the quote's `sourceAmount`.** Validation rejects mismatches; max is 1,000,000.
* **Idempotency:** `confirm-transfer` is idempotent on `tx_hash` — re-submitting the same hash won't double-pay out. Generate one `customer_uuid` per logical transfer.
* **Conditional fields:** skipping `requirements/refresh` when required leaves you with an incomplete recipient body → `400`.
* Recipient/wallet rejected by upstream sanctions screening surfaces as a `4xx` with a message — don't retry, surface it.

***

## KYC — on-chain identity attestation

`kyc.madhousewallet.com`

Verifies a real person via Stripe Identity (document + selfie) and writes a permanent attestation on-chain — **EAS** on Base/Polygon/Arbitrum, **SAS** on Solana. The wallet that pays the x402 charge is the wallet that gets bound to the attestation.

**Start a session** ($0.05). Body is empty — the payer wallet comes from the payment header.

```
POST /api/public/kyc/start      body: {}
→ { "kycUrl": "https://kyc.madhousewallet.com/verify/vs_1Taj…",
    "walletAddress": "0xef61…73f7" }
```

Open `kycUrl` in a browser to finish verification. A Stripe webhook then writes the attestation on-chain automatically — no further call needed from you.

**Check status** ($0.01). Authoritative: reads on-chain and checks revocation.

```
GET /api/public/kyc/status?wallet=0xef61…73f7
→ {
    "walletAddress": "0xef61…73f7", "verified": true, "chain": "base",
    "easUID": "0x9f8e…0100", "txHash": "0xa1b2…a1b2",
    "sasAttestationAddress": null, "verifiedAt": 1716500000,
    "verificationMethods": { "documentType": "passport", "selfieCheck": "verified" },
    "riskScore": "low"
  }
```

**Free verification check** — for gating logic at scale. DB-only, no on-chain read, no revocation check, rate-limited 60/min/IP.

```
GET /api/public/kyc/verified?wallet=0xef61…73f7
→ { "wallet": "0xef61…73f7", "verified": true }
```

**Wallet sanctions** ($0.02). Screens the *verified person* behind a wallet against OpenSanctions. PII is pulled transiently from Stripe and **never stored or returned** — only the match metadata comes back.

```
GET /api/public/kyc/sanctions?wallet=0xef61…73f7
→ { "walletAddress":"0xef61…73f7", "sanctioned": false, "matchCount": 0, "matches": [] }
```

**Edge cases**

* **`verified: true` requires the attestation to be on-chain AND not revoked.** A wallet mid-flow (`awaiting_stripe` / `attesting`) returns `verified: false`, not an error.
* **`/verified` (free) vs `/status` (paid):** use free for cheap gating; use paid `/status` when you need the revocation-checked source of truth.
* **`/sanctions` needs a completed attestation first** — screening a wallet with no on-chain KYC record returns `404`.
* **Pay-network ≠ attestation-chain.** The attestation chain is fixed at `/start` (EVM → Base by default; the `/solana` route → SAS on Solana). Paying `/status` on Polygon doesn't move the attestation.
* `start-eoa` is the free, signature-gated entry point: prove wallet ownership with a signed nonce (valid 60 s) instead of paying. Recovered signer must match `walletAddress` or you get `401`.

***

## Networks & assets

| Network  | CAIP-2                                    | USDC contract                                  |
| -------- | ----------------------------------------- | ---------------------------------------------- |
| Base     | `eip155:8453`                             | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`   |
| Polygon  | `eip155:137`                              | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359`   |
| Arbitrum | `eip155:42161`                            | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831`   |
| Solana   | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |

EVM endpoints accept all three EVM chains on the **main path**; Solana settlement uses the explicit `/solana` route. CDP sponsors gas on every chain, so your wallet only needs the listed USDC amount — no ETH/SOL for gas.

***

## Common status codes

| Code  | Meaning                                                                             |
| ----- | ----------------------------------------------------------------------------------- |
| `402` | Payment required — the client signs USDC and retries (AgentCash does this for you). |
| `400` | Bad input — missing param, malformed address/tx hash, unsupported currency.         |
| `401` | Signature didn't match the claimed wallet (`start-eoa`).                            |
| `404` | No record — e.g. KYC sanctions on an unverified wallet.                             |
| `429` | Rate limit — only on the free KYC endpoints.                                        |
| `503` | Upstream blip (sanctions DB / scanner) — retry with backoff.                        |

***

## Discovery

Every service publishes machine-readable discovery, which is what `discover_api_endpoints` reads:

* `GET /.well-known/x402` — list of payable resource URLs.
* `GET /openapi.json` — full OpenAPI 3.1 with `x-payment-info` (price, currency, network) and `x-payment-networks` per operation.

Endpoints are also auto-registered in the Coinbase **Bazaar** after their first settlement, so x402-aware agents can find them without a hardcoded list.

***

## Calling raw x402

Without AgentCash, use any x402 client (e.g. `@x402/fetch` with a funded signer):

```ts
import { wrapFetchWithPayment } from "@x402/fetch"
import { privateKeyToAccount } from "viem/accounts"

const account = privateKeyToAccount(process.env.PAYER_PRIVATE_KEY)
const fetchWithPay = wrapFetchWithPayment(fetch, account) // signs the 402 + retries

const res = await fetchWithPay(
  "https://scan.madhousewallet.com/api/public/quick-scan?address=vitalik.eth"
)
console.log(await res.json())
```

The signer needs only USDC on Base (or Polygon/Arbitrum/Solana) — gas is sponsored. For questions or integration support, contact <support@madhousewallet.com>.
