Pay per call. Nothing else.

Every service on BotTrunk is an HTTP endpoint that answers 402 Payment Required until it's paid in USDC. No API keys, no accounts, no invoices — the payment is the authentication.

How a paid call works

The x402 protocol turns the HTTP status code that was reserved for this in 1997 into a real payment flow. Four steps, about four seconds on Algorand:

  1. Ask. Your agent calls the endpoint. It gets a 402 whose body says what to pay, to whom, on which network.
  2. Pay. The agent signs a USDC transfer for that amount and retries the same request with a PAYMENT-SIGNATURE header.
  3. Verify & fulfil. We check the payment with the facilitator, then run the service upstream. Nothing is charged if the upstream fails.
  4. Settle. The transfer is submitted on-chain. The response carries a PAYMENT-RESPONSE header with the transaction id.

Connect your agent

Any x402-capable client handles the 402 → pay → retry dance for you. Pick the one that matches your stack — or follow the step-by-step setup for twelve agent clients.

# Claude Desktop, Claude Code, Cursor — any MCP host. Every live service becomes a tool.
npx bottrunk-mcp wallet          # creates ~/.bottrunk/wallet.json, prints the address
# then send that address 0.3 ALGO, and the USDC you want it to spend.
# The USDC opt-in in between happens by itself on the first tool call.

# Claude Code
claude mcp add bottrunk -- npx -y bottrunk-mcp

# Claude Desktop / Cursor (claude_desktop_config.json / .cursor/mcp.json)
{ "mcpServers": { "bottrunk": { "command": "npx", "args": ["-y", "bottrunk-mcp"] } } }

The MCP server keeps the key on your machine and enforces per-call and per-day caps (BOTTRUNK_MAX_PER_CALL, BOTTRUNK_MAX_PER_DAY); details in the bottrunk-mcp README on npm.

What your wallet needs. Build the transfer as an atomic group naming extra.feePayer and the facilitator covers the network fee, so the call costs you USDC only; sign a plain single transfer instead and you pay its ~0.001 ALGO. Either way the paying account must be opted in to USDC (31566704): an Algorand account needs 0.1 ALGO to exist and 0.1 more to hold an asset. Send 0.3 ALGO once — the spare covers fees — then USDC per call. An agent funded with USDC and no ALGO cannot pay at all, and USDC sent before the opt-in does not arrive. bottrunk-mcp performs the opt-in itself as soon as the ALGO lands, and refuses a paid call with an explanation rather than signing a payment the wallet cannot make.

Your agent cannot fund itself. There is no on-ramp inside the loop: a person sends it 0.3 ALGO and then the USDC, in that order. Fastest path of all if you already run an Algorand account — set BOTTRUNK_MNEMONIC to its 25 words and skip the funding entirely. An agent can spend only what you have already sent it. The caps ship at BOTTRUNK_MAX_PER_CALL=1000 and BOTTRUNK_MAX_PER_DAY=10000 USDC — deliberately high, because the catalog goes up to $414 a call. If your agent should never spend that much, set them lower before you fund the wallet.

Can't run a local command? https://mcp.bottrunk.com/mcp is a remote MCP server with five free, read-only tools — including bottrunk_payment_instructions, which returns the live price, network, asset and payTo for any service so your agent can price a call before committing to it. It holds no keys and can never spend. Setup for every client.

The 402 response

This is what scrape-markdown answers today when called without payment. amount is in atomic units — 90000 µUSDC is $0.09.

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "https://api.bottrunk.com/s/scrape-markdown",
    "description": "Any public page as clean, LLM-ready markdown.",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
      "asset": "31566704",
      "amount": "90000",
      "payTo": "UTWS33TM7IT7NINJSFWS5KVGL73G4ERJMYDKHF7KE4WDXHYO4L7V2PNMRE",
      "maxTimeoutSeconds": 60,
      "resource": "https://api.bottrunk.com/s/scrape-markdown",
      "description": "Any public page as clean, LLM-ready markdown.",
      "mimeType": "application/json",
      "extra": {
        "decimals": 6,
        "tag": "x402-global-challenge",
        "feePayer": "ZMFK2OI7ZBD2U27ISERZC4S6LKM6WMFJPZQ4MYNJDZ2VNBNMBA67RA22AA"
      }
    }
  ],
  "extensions": {
    "bazaar": {
      "info": {
        "input": {
          "type": "http",
          "method": "POST",
          "bodyType": "json",
          "body": {
            "url": "https://example.com/pricing",
            "render_js": false,
            "selector": "…"
          }
        },
        "output": {
          "type": "json",
          "example": {
            "markdown": "# Pricing\n\nSimple, honest pricing…",
            "title": "Pricing — Example",
            "word_count": 412
          }
        }
      },
      "schema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "input": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "const": "http"
              },
              "method": {
                "type": "string",
                "enum": [
                  "POST",
                  "PUT",
                  "PATCH"
                ]
              },
              "bodyType": {
                "type": "string",
                "enum": [
                  "json",
                  "form-data",
                  "text"
                ]
              },
              "body": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Public http(s) URL to fetch."
                  },
                  "render_js": {
                    "type": "boolean",
                    "description": "Reserved: headless rendering is not available yet; the flag is accepted and ignored."
                  },
                  "selector": {
                    "type": "string",
                    "description": "Optional CSS selector to scope the extraction."
                  }
                }
              },
              "pathParams": {
                "type": "object"
              }
            },
            "required": [
              "type",
              "method",
              "bodyType",
              "body"
            ],
            "additionalProperties": false
          },
          "output": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string"
              },
              "example": {
                "type": "object",
                "properties": {
                  "markdown": {
                    "type": "string",
                    "description": "Body content as GitHub-flavored markdown."
                  },
                  "title": {
                    "type": "string",
                    "description": "Document title."
                  },
                  "word_count": {
                    "type": "integer",
                    "description": "Words in markdown, for budgeting tokens."
                  }
                }
              }
            },
            "required": [
              "type"
            ]
          }
        },
        "required": [
          "input"
        ]
      }
    }
  }
}
scheme string Always exact: pay exactly the amount, nothing is metered.
network string CAIP-2 id of the chain. Algorand MainNet or TestNet.
asset string USDC asset id on that network.
amount string Price in atomic units (6 decimals).
payTo string BotTrunk's receiving address. Never send anywhere else.
maxTimeoutSeconds integer How long the payment stays valid.
extensions.bazaar object Input/output schema so agents can discover and call the service without reading this page.

The payment header

PAYMENT-SIGNATURE is base64 of a JSON object carrying the signed Algorand transaction group. Clients build it; you only need to know the shape if you're writing one.

{
  "x402Version": 2,
  "accepted": {
    "scheme": "exact",
    "network": "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=",
    "asset": "31566704",
    "amount": "90000",
    "payTo": "UTWS33TM7IT7NINJSFWS5KVGL73G4ERJMYDKHF7KE4WDXHYO4L7V2PNMRE",
    "maxTimeoutSeconds": 60,
    "extra": { "decimals": 6, "tag": "x402-global-challenge", "feePayer": "ZMFK2OI7ZBD2U27ISERZC4S6LKM6WMFJPZQ4MYNJDZ2VNBNMBA67RA22AA" }
  },
  "payload": {
    "paymentGroup": ["<base64 msgpack of each signed transaction>"],
    "paymentIndex": 0
  },
  "resource": { "url": "https://api.bottrunk.com/s/scrape-markdown" }
}

Note the shape. In x402 v2 the scheme and network live inside accepted — echo back the entry from accepts[] that you chose. The older v1 shape, with scheme and network at the top level, is still accepted. paymentIndex points at your transfer inside paymentGroup: it is 0 for a plain single transfer, and the index of your transfer in the group when you build a fee-payer group.

On success the response carries PAYMENT-RESPONSE: base64 JSON with success, transaction (the Algorand txn id), network and payer.

Both header vocabularies work, everywhere

x402 v2 renamed the headers; we read and emit both, so a v1 client needs no changes. Requests: PAYMENT-SIGNATURE, alias X-PAYMENT — v2 wins if you send both. The 402 also carries PAYMENT-REQUIRED, the base64 of the same challenge body, so you can read the price without parsing JSON. The 200 carries PAYMENT-RESPONSE and X-PAYMENT-RESPONSE with identical values.

CORS is open (Access-Control-Allow-Origin: *) with the payment headers allowed on the request and exposed on the response, so a browser wallet or dApp can pay these endpoints directly.

Networks

NetworkCAIP-2 idUSDC asset
Algorand Mainnet algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8= 31566704

MainNet is the only network served. The code supports TestNet, but there is no public TestNet endpoint, so the smallest real call is $0.02 — rehearse for free with the 402 probe or bottrunk_payment_instructions instead.

Payments are verified and settled by the GoPlausible facilitator. BotTrunk never holds your keys and never holds your funds: the transfer goes from your wallet to the receiving address in one on-chain transaction.

Errors, limits and retries

The contract an agent needs in order to fail safely. Every one of these strings is what the gateway actually returns.

Statuserror in the bodyCharged
200 Yes. PAYMENT-RESPONSE carries the transaction id.
402 Payment required No — you sent no payment header. This is the probe.
402 missing or malformed X-PAYMENT header, unsupported x402 version, scheme mismatch, network mismatch, or the facilitator's own invalidReason No. Verification refused it.
402 settlement failed, or an explained node error — "the paying wallet is not opted in to USDC…", "at its Algorand minimum balance…", "the signed payment expired…" No — but the work was done. See below.
422 A validation message: url must be a public http(s) URL, url resolves to a private address, selector "…" matched nothing on this page, and so on. No.
502 fetch failed with N, page too large, fetch failed: … No. The upstream let us down.
503 <name> is not live yet, with status and catalog No. Listed for discovery, not callable.
The one retry that can cost you twice

Algorand's own errors reach you through the facilitator; we translate the ones we know into an instruction and keep the node's original in the message, so asset 31566704 missing from … arrives as "the paying wallet is not opted in to USDC" rather than as a log line.

A 402, 422, 502 or 503 means nothing settled, so retrying is free. settlement failed is different: verification passed, the service already ran, and the transfer did not settle as far as we could tell. If it did land on-chain anyway, signing a fresh payment pays for the same work twice. Check the payer address on an explorer before re-signing. An agent whose rule is "on 402, sign and retry" should make an exception for this string.

Client timeout Allow at least 30 seconds. Fetch plus verify plus settle measures about 4.6 s end to end, and built-in services time out at 20 s on their own. The per-service latency published on this site measures fulfilment only — it does not include settlement, so do not budget from it.
Rate limits None published. Be reasonable; abusive traffic is blocked at the edge rather than metered.
Idempotency A signed payment is consumed when it settles, so never replay the same signature after a client-side timeout. Check the explorer first, then sign a fresh payment if nothing landed.
Catalog status live services are priced and callable. on_request services are real work done by people, arranged by email first; their endpoints answer 503, so nothing can be charged by accident.

Catalog API

The same list as the home page, for machines. Public, no payment needed.

curl https://api.bottrunk.com/api/v1/catalog                 # every service
curl https://api.bottrunk.com/api/v1/catalog?category=Data   # filtered
curl https://api.bottrunk.com/api/v1/catalog/scrape-markdown  # one, with input/output schema