Skip to main content
Glama

metered-mcp

An MCP server that charges per call over HTTP 402, and attaches auditable proof to every answer.

Two things an AI agent needs before it can be a paying customer, and neither exists yet:

  • A way to pay. Agents cannot enter a card number or negotiate a contract. They need to pay a fraction of a cent per request, without an account.

  • A way to trust the answer. Paying for a number is worthless if the agent cannot check it against something.

This repo is a working implementation of both, in about 1,100 lines of application code.

$ python client/agent_client.py

[1] tools/list (free)
    list_sites       free
    query_monthly    0.002 USDC
    find_anomalies   0.01 USDC
    verify_claim     0.001 USDC

[3] query_monthly (paid)
  query_monthly: 402 payment required, 2000 units of USDC
  query_monthly: 200 paid, receipt 7e6bd95669782c27
    total_kwh=1484544.0  observations=12
    provenance: 12 rows cited, self-check={'field': 'total_kwh', 'recomputed': 1484544.0, 'match': True}

[6] replay the same payment
    402 nonce_replayed: this payment nonce has already been spent

[7] pay for query_monthly, then call find_anomalies
    402 request_mismatch: this payment authorizes a different request than the one being made

Quickstart

python -m venv .venv && . .venv/bin/activate    # Windows: .venv\Scripts\activate
pip install -r requirements.txt

python scripts/make_dataset.py                  # writes data/sites_monthly.csv
uvicorn app.main:app --port 8000                # in one terminal
python client/agent_client.py                   # in another

Tests (19, none of them touch the network):

python -m pytest

Open http://127.0.0.1:8000/ for the human-readable landing page, /pricing for the machine-readable one.


Related MCP server: agent-x402-paywall-mcp

How the payment flow works

client                                   server
  │  POST /mcp {tools/call: query_monthly}  │
  ├───────────────────────────────────────>│
  │                                        │  no PAYMENT-SIGNATURE
  │  402 + PAYMENT-REQUIRED: <base64>      │
  │<───────────────────────────────────────┤  {amount, asset, payTo,
  │                                        │   network, requestDigest}
  │                                        │
  │  signs an authorization bound to       │
  │  sha256(tool + arguments)              │
  │                                        │
  │  POST /mcp + PAYMENT-SIGNATURE         │
  ├───────────────────────────────────────>│
  │                                        │  verify → consume nonce
  │                                        │  → execute → record
  │  200 + PAYMENT-RESPONSE: <receipt>     │
  │<───────────────────────────────────────┤

The 402 body and the PAYMENT-REQUIRED header carry the same JSON: what to pay, in which asset, to which address, on which network, valid for how long, and which request the payment is for.

Prices are published before anyone calls anything — tools/list returns _meta.x402.price per tool. An agent that cannot see prices before calling cannot budget, which makes it not an agent but a runaway loop.


Security properties (each one has a test)

Attack

Defence

Test

Spend one payment twice

Nonce consumed by a PRIMARY KEY insert; the second insert loses at the database level, not in a Python set

test_replayed_nonce_is_rejected

Pay for a cheap call, invoke an expensive one

Authorization carries resource = sha256(tool+args); a mismatch is rejected

test_payment_cannot_be_spent_on_a_different_request

Pay less than the price

value >= maxAmountRequired, checked before anything runs

test_invalid_payments_are_rejected[underpaid]

Present an expired or not-yet-valid authorization

validAfter / validBefore window is enforced server-side

...[expired], ...[not_yet_valid]

Forge a signature

hmac.compare_digest, constant-time

...[bad_signature]

Pay on a different chain

network must match the challenge exactly

...[network_mismatch]

Pay someone else

to must equal payTo

...[payto_mismatch]

Get charged for a request that cannot run

Full semantic validation happens before the paywall

test_bad_arguments_are_rejected_before_payment_is_requested

That last row is the one people skip. If you validate after taking payment, a typo in a site_id costs the caller a nonce and the operator a refund. The fix is architectural: every tool has a normalize() that does the complete check, and the server runs it before touching the payment path.


Answer provenance

Every result carries a block like this:

"provenance": {
  "dataset": "sites_monthly.csv@sha256:a298374c2153fa13",
  "rows": [13, 14, 15],
  "computed": {"total_kwh": 1484544.0, "observations": 12},
  "method": "sum kwh over rows where site_id == cn-east-01",
  "selfCheck": {"field": "total_kwh", "recomputed": 1484544.0, "match": true}
}

Three properties matter:

  1. Row-level citations. Not "the dataset" — the exact line numbers. A caller can re-read them.

  2. Dataset fingerprint. If the file changes, every previous answer is visibly from a different version.

  3. A self-check that recomputes the headline number from the cited rows. This catches the failure that matters most: a number that is internally consistent but does not correspond to the rows the answer points at.

verify_claim exposes the same machinery as a tool. It exists for one reason: an agent should be able to check a number before repeating it to a human.

{"metric": "total_kwh", "expected": 2226816.0, "computed": 1484544.0,
 "delta": -742272.0, "match": false, "tolerance": 2.2e-6}

The dataset

data/sites_monthly.csv is synthetic — 6 sites × 36 months of monthly kWh, generated by scripts/make_dataset.py with a fixed seed. No real facility, customer or grid data is involved.

Three defects are planted on purpose, and find_anomalies recovers exactly those three with no false positives:

Site

Month

Kind

cn-east-02

2024-03

negative reading (meter rollover)

cn-south-01

2025-07

6× spike

cn-west-02

2024-11

missing reading

This is deliberate. A demo dataset with no ground truth cannot show that detection works — it can only show that it runs. Swap the file and the fingerprint, the tests and the provenance all continue to work.


What is real, and what is a demo

Being precise about this is the difference between a demo and a lie.

Real: the HTTP surface, the challenge/response structure, the check ordering, the request binding, the replay guard, the receipt format, the provenance and self-check, the validation-before-charge design.

Not real: settlement. sign_authorization() uses HMAC with a shared secret so the repo runs with zero keys, zero chains and zero network. A production verifier validates an EIP-3009 TransferWithAuthorization signature against the token contract and settles through a facilitator; x402.verify_payment() is the single function to replace. The receipt says "settlement": "simulated" rather than pretending.

Also not implemented: refunds. Payment is taken after validation and before execution, so the only way to lose money is a bug inside the handler itself.


Layout

app/
  config.py     prices, network, payee — all env-overridable
  x402.py       challenge, checks, replay guard, receipts
  ledger.py     SQLite: spent nonces, issued receipts
  data.py       CSV load, row numbering, robust statistics
  tools.py      schemas, normalisers, handlers, provenance
  main.py       FastAPI routes + JSON-RPC dispatch
client/
  agent_client.py   a reference client that pays for what it uses
scripts/
  make_dataset.py   deterministic synthetic data with planted defects
tests/
  test_flow.py      19 tests: happy path, 8 rejection paths, integrity

Design notes

Why the price table lives in code, not in the database. Prices are part of the advertised contract; a client caches them. Putting them behind a mutable store invites the case where a client budgets 0.002 and gets charged 0.01. Change them by deploying.

Why list_sites is free. Discovery must never cost anything. A metered discovery endpoint produces agents that guess instead of looking.

Why modified z-score instead of standard deviation. One 6× spike inflates stdev enough to hide itself — the outlier rewrites the baseline that is supposed to catch it. Median absolute deviation is not moved by it. The same principle applies to graders: a scoring function must be robust against exactly the failure it is meant to detect.

Why the nonce insert is a database constraint. A set() in process memory is correct only with one worker. The PRIMARY KEY insert is correct with any number of them, and it costs nothing.


Roadmap

  • Swap sign_authorization for an EIP-3009 verifier behind the same seam

  • Streamable HTTP transport so a stock MCP client can connect without a shim

  • Refund path: hold the nonce, release it if the handler raises

  • Publish to an x402 discovery directory so agents can find it

  • Rate limiting per payer, not per IP


中文说明

这是一个按次收费的 MCP 服务:AI 智能体调用工具时遇到 402 Payment Required,签一笔授权、重试,拿到带溯源信息的结果。

三个设计要点:

  1. 先校验再扣款 —— 参数不合法在收费之前就拒绝,不让调用方为一个跑不通的请求付钱。

  2. 付款绑定到具体请求 —— 授权里带着 sha256(工具名+参数),付便宜的调用去执行贵的调用会被拒。

  3. 每个答案都可追溯 —— 返回结果附数据集指纹、精确行号,并独立重算一次主数字做自检。

数据集是合成的(6 个站点 × 36 个月用电量),故意埋了 3 处异常,find_anomalies 能无误差地全部找出。结算部分是模拟的,没有真实资金、没有链上交互,接生产环境需要把 verify_payment 换成 EIP-3009 验签。

License

MIT.

Related MCP Connectors

Related MCP Servers