MCP / Agent tools

Connect your agent to x402 payments.

One MCP endpoint for checking supported networks, matching payment options, verifying authorizations, and submitting signed payments.

Endpoint
https://facilitator.ultravioletadao.xyz/mcp
Transport
Streamable HTTP · stateless · POST
Authentication
none — the payer's signature is the only authority
Price
0% — this endpoint never answers 402

Connect your client

Streamable HTTP, and only that. The server is stateless: every JSON-RPC message is an independent POST, no mcp-session-id is issued, and tools/call works without a previous initialize. Nothing lives in memory between two calls, because the next one may land on a different task.

Claude Code

claude mcp add --transport http x402-facilitator \
  https://facilitator.ultravioletadao.xyz/mcp

Claude Desktop, and any client with a config file

{
  "mcpServers": {
    "x402-facilitator": {
      "type": "streamable-http",
      "url": "https://facilitator.ultravioletadao.xyz/mcp"
    }
  }
}

By hand

The handshake is optional on a stateless server, but it is the shortest call that proves the endpoint is reachable and tells you which release is answering:

curl -sS https://facilitator.ultravioletadao.xyz/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"my-agent","version":"1.0"}}}'
{"jsonrpc":"2.0","id":1,"result":{
  "protocolVersion":"2025-06-18",
  "capabilities":{"tools":{}},
  "serverInfo":{
    "name":"x402-facilitator",
    "title":"x402 Payment Facilitator",
    "version":"<the running release>",
    "websiteUrl":"https://facilitator.ultravioletadao.xyz"},
  "instructions":"Call x402_supported first ..."}}

Both Accept types, or 406. The Streamable HTTP transport rejects a request whose Accept does not name both application/json and text/event-stream — even though this server never opens a stream and always answers JSON. It is the first wall an integrator writing a client by hand runs into, and the refusal is a 406 with no other explanation.

A Host header outside the allowlist gets a 403 before anything else runs. That check is DNS-rebinding protection aimed at MCP servers on a developer's laptop; the production host is built into the default rather than left to configuration, because the library's own default is loopback only, which behind a load balancer rejects every real request while local testing looks perfect.

A GET on this path is this page. A client that asks for application/json or text/event-stream still gets the 405 that tells it to POST, because a client that lands here with that Accept is an MCP client that used the wrong method and needs the machine answer, not a page written for a person. Accept: text/markdown returns the same guide as Markdown.

What the server does

This service verifies signed x402 payments and submits them on their native network, paying network fees. The buyer signs the payment; the facilitator sponsors its submission. Discover the enabled capabilities before choosing an offer.

Four things this server is not.

  • Not a wallet. It holds no key of yours, signs nothing on your behalf and cannot move a coin you did not authorize. The signature inside the payload is the only authority there is, which is also why there is nothing to log in to.
  • Not a paid API. The MCP endpoint charges nothing and never answers 402. The only money that moves is the buyer's payment going to the seller.
  • Not a second implementation. Each tool runs the request through the facilitator's own REST router — the same handler, the same rate limiter, the same writer lease. There is no code path here that could answer a different truth from the HTTP door.
  • Not an admin console. Revoking feedback, suppressing a bazaar resource, minting an identity and the DX402 writes are all deliberately absent from the tool list. An MCP client is a language model holding a menu, and a tool that erases somebody else's reputation does not belong on one.

The payment workflow

It is not "call an endpoint and read a number". Each step answers a question the next one assumes:

x402_supported()            ← can you settle THIS scheme on THIS network?
                              the only authoritative answer. takes no arguments.
                              read it before building a payment, not after.
      │
x402_accepts(accepts)       ← the seller offered N ways to pay in its 402.
                              which of them can this facilitator actually
                              settle? comes back enriched with what it knows:
                              feePayer, token list, escrow addresses.
                              moves nothing, signs nothing.
      │
      │  the buyer signs the network-native payment for one offer
      │
x402_verify(payload, reqs)  ← would this settle? signature, nonce, amount,
                              timestamps, token and network support.
                              SUBMITS NOTHING. this is the dry run.
      │
      ├─ isValid false → read errorReason. some are permanent
      │                   (bad signature), some are not (RPC down).
      │
x402_settle(payload, reqs)  ← broadcast. real funds. irreversible.
                              send idempotencyKey on every retry.

Available tools

Tool Is Arguments Moves money
x402_supported GET /supported none no
x402_accepts POST /accepts accepts, x402Version, error no
x402_verify POST /verify x402Version, paymentPayload, paymentRequirements no
x402_settle POST /settle the same three, plus the optional idempotencyKey yes, irreversibly

The arguments of each tool are the JSON body of the request it stands for, and the result is that request's response body verbatim, in a single text content block. Nothing is reshaped on the way through, so everything written about /verify and /settle in the agent manual is true over MCP too.

x402_supported — start here

curl -sS https://facilitator.ultravioletadao.xyz/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{
        "name":"x402_supported","arguments":{}}}'

result.content[0].text contains the live GET /supported response: versions, schemes, networks, aliases, tokens and network-specific fee payers. Arc supports direct EOA USDC/EURC exact v1/v2. Hedera uses hedera:mainnet or hedera:testnet, native v2/exact only. Read the native envelope in the agent manual; legacy names are not universal.

x402_settle — the one that spends

This moves real funds and cannot be undone. It broadcasts the payer's authorization as a blockchain transaction. Nothing in this facilitator and nothing on any chain reverses a confirmed transfer. Call x402_verify first — it checks the same signature, the same nonce, the same amount and the same network, and submits nothing.

A tool call cannot set headers, and that is the one real gap between this door and the HTTP one. It is a gap in capability, never in privilege: nothing here can move funds an HTTP client could not. The input that mattered got an argument instead — idempotencyKey is lifted out of the body and sent as the Idempotency-Key header, so a retry after an ambiguous failure settles once and not twice. Send a fresh unguessable value per payment (a UUIDv4 is the right shape) and reuse it on every retry of that payment. Keys share one namespace across all callers: a predictable one like retry-1 can be claimed by somebody else's settle first, and yours is then refused with 409. The v2 PAYMENT-SIGNATURE header transport has no MCP equivalent; put the payload in the body.

Handle payment errors

When the underlying REST call answers 4xx or 5xx, the tool returns a normal result with isError: true carrying the facilitator's own body verbatim. It is deliberately not a JSON-RPC error, which most clients render as an opaque "internal error" and would hide invalid signature behind:

{"result":{"isError":true,"content":[{"type":"text","text":
  "{\"error\":\"Failed to deserialize VerifyRequest: data did not match any
     variant of untagged enum VerifyRequestEnvelope\",
    \"code\":\"invalid_request_body\",\"hint\":\"...\"}"}]}}

A JSON-RPC error is reserved for what never reached a tool at all — an unroutable call:

{"jsonrpc":"2.0","id":8,"error":{"code":-32601,
  "message":"unknown tool: nope",
  "data":{"tools":["x402_supported","x402_accepts","x402_verify","x402_settle"]}}}

Rate limits

This endpoint does not have a budget of its own. It draws on the same per-IP bucket as POST /verify and POST /settle, because it is the same handlers being called. Mixing the two doors does not buy more room; it spends the same allowance twice as fast.

HTTP/1.1 200 OK
x-ratelimit-limit: 30
x-ratelimit-remaining: 25

Every response carries those headers, and a refusal is a 429 with retry-after and a JSON body. Throttle on the headers rather than on the failure: by the time a 429 arrives, the call already cost a round trip.

Client compatibility notes

  • Both Accept types or nothing. accept: application/json, text/event-stream, always. One of the two alone is a 406 with no hint, on a server that never streams.
  • A tool call carries no headers. Anything the HTTP door reads from a header has either an argument here or no equivalent at all. idempotencyKey is the argument; the v2 PAYMENT-SIGNATURE transport is the no-equivalent.
  • An ambiguous settle is not a failed settle. A timeout means the transaction may already be on its way. Retry with the same idempotencyKey — that is what turns "I do not know" into exactly-once instead of into a double payment.
  • x402_verify answering isValid: false is not always permanent. A bad signature is; an RPC that could not be reached is not. Read errorReason before deciding whether to give up or retry.
  • Discover capabilities before paying. Read x402_supported for versions, schemes and network identifiers. Aliases are not distinct networks, and native Hedera has no v1 alias.
  • GET /mcp is not the server. It is this page. A client that GETs with a JSON or event-stream Accept gets a 405 pointing at POST; if your client "connected" but no tool ever appears, check that it is POSTing.
  • The version in the handshake is stamped at runtime. serverInfo.version is not written in any file — a number typed into a discovery document is stale the first release nobody remembers to bump, and a client that caches the card believes it.