Give your agent four tools that pay for things
One stateless HTTP endpoint, nothing to install, no API key and no charge — and of the four tools exactly one moves real money.
- 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
What you get here, and what you do not
What is being offered is settlement, not information. This service verifies an x402 payment authorization against the chain it names and then broadcasts it, paying the gas out of its own wallet, on seven chain families. The buyer signs; the buyer never holds native tokens for gas. That is the whole product, and the four tools are the whole surface of it.
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.
One transport, one server
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.
The decision loop
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 an EIP-3009 authorization for one of them
│
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.
The four tools, and only four
| 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 is the exact body of GET /supported: every (scheme, network) pair this facilitator settles, each network spelled twice — the x402 v1 name ("base") and the CAIP-2 form ("eip155:8453") — with the token list for each. Both spellings are accepted everywhere a network is named. Never hard-code the count of networks from this page or any other: this call is the only answer that is true today.
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.
A failed call is a result, not a protocol error
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"]}}}
The rate limit is shared with /verify and /settle
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.
Measured traps
- 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.
idempotencyKeyis the argument; the v2PAYMENT-SIGNATUREtransport 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_verifyansweringisValid: falseis not always permanent. A bad signature is; an RPC that could not be reached is not. ReaderrorReasonbefore deciding whether to give up or retry.- Do not hard-code a network count. Not from this page, not from the landing page, not from the server card.
x402_supportedis the only answer that is true today, and it lists every network twice on purpose. GET /mcpis not the server. It is this page. A client that GETs with a JSON or event-streamAcceptgets 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.versionis 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.
Everything next to this server
| What | Where |
|---|---|
| MCP server card | /.well-known/mcp/server-card.json |
| The agent manual — the same four calls over HTTP | /skill.md |
| How paying works, in detail | /auth.md |
| OpenAPI, the live schema | /openapi.json · /docs |
| Networks and schemes, right now | /supported |
| llms.txt of this host | /llms.txt · /llms-full.txt |
| Source | github.com/UltravioletaDAO/x402-rs |