# Skill: Handle 402 Payment Required Responses

> When an API call requires payment and you have no credits, the proxy returns a 402 with instructions. This skill explains how to parse and respond to it.

## Can you skip 402 handling entirely?

Yes - if you use credits, the proxy never returns a 402. Top up at https://apihub.io/agents/credits or run `npx @apihubio/cli topup 10`. Every call after that drains from your balance without any signing ceremony. This skill is only needed if you want to sign x402 payments yourself.

For external x402 APIs, you can also skip client-side 402 handling by calling them through https://proxy.apihub.io/external (or the `apihub_call_external` MCP tool) - APIHub's platform wallet signs for you and debits your credits.

## When do you get a 402?

A 402 is returned when:
- You have no prepaid credits
- The endpoint is not free-tier
- No PAYMENT-SIGNATURE header was included

## 402 response format

```json
{
  "x402Version": 1,
  "accepts": [{
    "scheme": "exact",
    "network": "base",
    "maxAmountRequired": "1000",
    "resource": "https://proxy.apihub.io/weather-api/current",
    "description": "Current weather for a location",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0xProviderWallet",
    "maxTimeoutSeconds": 300,
    "extra": {
      "name": "USD Coin",
      "version": "2"
    }
  }],
  "error": "Payment required"
}
```

The response also includes a base64-encoded header:
```
PAYMENT-REQUIRED: <base64 JSON of the accepts array>
```

## Key fields

| Field | Meaning |
|-------|---------|
| maxAmountRequired | Cost in USDC atomic units (6 decimals). 1000 = $0.001 |
| asset | USDC contract address on Base |
| payTo | Wallet to send payment to |
| network | Blockchain network (always "base" on APIHub) |
| extra.name | Token name for EIP-712 domain ("USD Coin") |
| extra.version | Token version for EIP-712 domain ("2") |

## Option 1: Use @x402/fetch (recommended)

The simplest approach. The library handles 402 detection, signing, and retry automatically:

```typescript
import { wrapFetchWithPayment } from "@x402/fetch";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const account = privateKeyToAccount("0xYOUR_KEY");
const walletClient = createWalletClient({ account, chain: base, transport: http() });
const x402Fetch = wrapFetchWithPayment(walletClient);

// Automatically handles 402 challenge-response
const res = await x402Fetch("https://proxy.apihub.io/weather-api/current?location=London", {
  headers: { "Authorization": "Bearer ahk_your_key" },
});
```

## Option 2: Handle manually

### Step 1: Detect the 402

```typescript
const res = await fetch("https://proxy.apihub.io/weather-api/current?location=London", {
  headers: { "Authorization": "Bearer ahk_your_key" },
});

if (res.status === 402) {
  const challenge = await res.json();
  const requirements = challenge.accepts[0];
  // Proceed to step 2
}
```

### Step 2: Sign EIP-3009 TransferWithAuthorization

```typescript
const nonce = "0x" + Array.from(crypto.getRandomValues(new Uint8Array(32)))
  .map(b => b.toString(16).padStart(2, "0")).join("");
const now = Math.floor(Date.now() / 1000);

const authorization = {
  from: account.address,
  to: requirements.payTo,
  value: String(requirements.maxAmountRequired),
  validAfter: String(now - 60),
  validBefore: String(now + 600),
  nonce,
};

const signature = await walletClient.signTypedData({
  domain: {
    name: requirements.extra.name,       // "USD Coin"
    version: requirements.extra.version, // "2"
    chainId: 8453n,                      // Base
    verifyingContract: requirements.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: authorization,
});
```

### Step 3: Retry with PAYMENT-SIGNATURE header

```typescript
const paymentPayload = {
  x402Version: 1,
  scheme: "exact",
  network: "base",
  payload: { signature, authorization },
};

const paymentSig = btoa(JSON.stringify(paymentPayload));

const paidRes = await fetch("https://proxy.apihub.io/weather-api/current?location=London", {
  headers: {
    "Authorization": "Bearer ahk_your_key",
    "PAYMENT-SIGNATURE": paymentSig,
  },
});

// Now you get the actual API response
const data = await paidRes.json();
```

## Option 3: Buy credits instead

If you want to avoid per-request gas costs, purchase credits:

```
POST https://api.apihub.io/v1/credits/purchase
```

See https://apihub.io/skills/purchase-credits.md for the full flow.

Once you have credits, the same request works without any PAYMENT-SIGNATURE -- credits are debited automatically.

## Common errors after payment

| Status | Meaning |
|--------|---------|
| 200 | Payment accepted, here is your response |
| 400 | Payment signature malformed or amount mismatch |
| 400 | Insufficient USDC balance in your wallet |
| 500 | On-chain settlement failed (try again) |

## Related skills

- Purchase credits to avoid 402s: https://apihub.io/skills/purchase-credits.md
- Make API calls: https://apihub.io/skills/make-api-call.md
