Skip to content
MCP & AI agents

Pay with x402

Let AI agents pay per request with USDC on Base instead of an API token.

AI agents can pay for Scrape.ly API calls with USDC on Base using the x402 protocol instead of an API token. The agent needs a wallet with USDC. It does not need a Scrape.ly account: one is created for the wallet on its first payment.

How it works

  1. The client sends a request with the header x-scrapely-payment-protocol: x402 and no payment.
  2. The API answers 402 Payment Required. The payment-required response header holds the price, network, and receiving address as base64 JSON.
  3. The client signs a USDC transfer for that amount and sends the same request again with the signature in the PAYMENT-SIGNATURE header.
  4. The API checks the signature and handles the request as the wallet's account.

Payments top up credit. The API collects a payment on-chain and adds it to the wallet's account as credit only when the account needs it: when its credit is low, or after a run was refused for insufficient credit. While the account has enough credit, a signed payment only identifies the wallet and no funds move. Actor runs are charged from that credit, and set their estimated cost aside before they start. Credit that is not used stays on the account for later requests.

Each signed payment works for one request. Signing is free, so sign a new one for every request, or use the session token for follow-up requests.

Payment requirements

.bash
curl -i -H "x-scrapely-payment-protocol: x402" "https://api.scrape.ly/v2/actors/"

Decoded payment-required header:

.json
{
  "x402Version": 2,
  "error": "X402 payment required",
  "resource": { "url": "https://api.scrape.ly" },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "1000000",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "payTo": "0x…",
      "maxTimeoutSeconds": 60,
      "extra": { "name": "USD Coin", "version": "2", "displaySymbol": "USDC" }
    }
  ]
}
FieldDescription
networkCAIP-2 network id: eip155:8453 for Base, eip155:84532 for Base Sepolia.
amountTop-up amount in USDC base units (6 decimals). 1000000 is 1 USDC.
assetUSDC contract on that network.
payToScrape.ly's receiving wallet.
extra.name, extra.versionThe USDC contract's EIP-712 domain. The signature must use these values: USD Coin on Base, USDC on Base Sepolia.

Read these values from the response instead of hard-coding them. The same requirements apply to every endpoint.

Sign and send a payment

The signature is an EIP-3009 TransferWithAuthorization for the USDC contract, sent as base64 JSON. The official x402 client libraries build it for you:

.bash
npm install @x402/core @x402/evm viem
.js
import { x402Client } from '@x402/core/client'
import { registerExactEvmScheme } from '@x402/evm/exact/client'
import { toClientEvmSigner } from '@x402/evm'
import { privateKeyToAccount } from 'viem/accounts'
import { createPublicClient, http } from 'viem'
import { base } from 'viem/chains'

const account = privateKeyToAccount(process.env.WALLET_PRIVATE_KEY)
const client = registerExactEvmScheme(new x402Client(), {
  signer: toClientEvmSigner(account, createPublicClient({ chain: base, transport: http() })),
})

const url = 'https://api.scrape.ly/v2/actors/'
const headers = { 'x-scrapely-payment-protocol': 'x402' }

const unpaid = await fetch(url, { headers })
const required = JSON.parse(atob(unpaid.headers.get('payment-required')))
const payment = await client.createPaymentPayload(required)

const res = await fetch(url, {
  headers: { ...headers, 'PAYMENT-SIGNATURE': btoa(JSON.stringify(payment)) },
})

The wallet pays no gas: the payment is collected through an x402 facilitator.

The signed authorization must have a nonce of 0x followed by 64 hex characters, and a validBefore that is a whole number of seconds at most maxTimeoutSeconds plus 5 minutes from now. The x402 client libraries do both by default. Other payments are refused before they are checked.

Session token

Every response to an authenticated x402 request has an x-scrapely-x402-session header. Send it back with x-scrapely-payment-protocol: x402 instead of PAYMENT-SIGNATURE, and the request runs as the same wallet account for the next 10 minutes without a new payment:

.js
const session = res.headers.get('x-scrapely-x402-session')
await fetch('https://api.scrape.ly/v2/actor-runs/RUN_ID', {
  headers: { ...headers, 'x-scrapely-x402-session': session },
})

Treat the token like an API key: anyone who has it can use the account's credit until it expires.

The wallet's account

The first payment from a wallet creates a Scrape.ly account for it and is always collected. Later requests from the same wallet use the same account, so its runs, datasets, and other storage stay together. Collected payments are added to the account as pay-as-you-go credit and are not refunded.

If a run cannot set aside enough credit, the API answers 402 payment-required with the payment-required header. Sign a new payment and retry: once the account has less than one top-up of credit left, that payment is collected as a top-up. Retry again if one top-up is not enough for the run. While the account still has more credit than one top-up, a refused run collects nothing.

x402 payments act for the account but cannot use account endpoints: /v2/users/me/*, API keys, earnings, payouts, monetization, conversations, schedule changes, and the other endpoints that need a user API key. These return 403 insufficient-permissions before the payment is checked, so the signature is not spent and can still pay for another request. Session tokens get the same 403. See Users.

Actor owners can stop AI agents from running their Actor with allowsAgenticUsers (see Update Actor). Such an Actor returns 403 agentic-users-not-allowed to x402 payments and to the wallet's account on every start path: runs, saved tasks, schedules, automations, Standby, metamorph, and resurrect.

MCP server

Connect to https://api.scrape.ly/mcp?payment=x402 without a token. Tools that call the API are marked with _meta.x402 and the payment requirements. Pass the payment in the tool call's _meta["x402/payment"], as the payment object or its base64 string. A call without a payment returns the requirements as an error result, so the client can sign and retry. One payment covers the whole tool call, including call-actor waiting for the run and reading its results. See MCP server.

Errors

Each row except the last returns 402 with error type payment-required and the payment-required header.

MessageCause
Payment required. Provide a valid x402 payment in the PAYMENT-SIGNATURE header.No PAYMENT-SIGNATURE header.
The PAYMENT-SIGNATURE header is not valid base64-encoded x402 payment data.The header is not base64 JSON.
The provided x402 payment is invalid, already used, or does not match the payment requirements.Wrong signature, amount, receiver, network, or token name, not enough USDC, or the authorization expired.
This x402 payment has already been used.The signature already paid for a request.
The x402 payment must carry one signed authorization with a from address and nonce.The authorization has no from, its nonce is not 0x plus 64 hex characters, or the payment carries two different authorizations.
The x402 authorization validBefore may be at most 60 seconds ahead.validBefore is more than maxTimeoutSeconds plus 5 minutes away, or is not a whole number of seconds.
Payment settlement failed: …A top-up was needed but could not be confirmed. Usually no funds moved. If they did, resend the same payment once to add it as credit. Then sign a new payment for the request.
This x402 payment was credited to the wallet account but cannot authenticate a request. Send a new x402 payment.A resent payment whose earlier collection had not been credited. It is now credit; sign a new payment for the request.
The x402 session has expired or is not valid. Send a new x402 payment.The session token is older than 10 minutes or unknown.
Insufficient funds. Estimated cost: …A run could not set aside enough credit. Retry with a new payment to top up.
403 insufficient-permissionsThe endpoint needs a user API key. The payment is not collected.