Metadata-Version: 2.4
Name: pontage
Version: 0.1.0
Summary: Self-settling x402 payment gateway for MCP tools — verify and settle EIP-3009 USDC payments directly against the chain, no facilitator.
Project-URL: Homepage, https://github.com/mteen1/pontage
Project-URL: Repository, https://github.com/mteen1/pontage
Project-URL: Issues, https://github.com/mteen1/pontage/issues
Author-email: Matin Moharrami <matinmoharrami11@gmail.com>
License: MIT
License-File: LICENSE
Keywords: ai-agents,eip-3009,mcp,micropayments,payments,usdc,x402
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.12
Requires-Dist: aiosqlite>=0.20
Requires-Dist: alembic>=1.13
Requires-Dist: asyncpg>=0.29
Requires-Dist: eth-account>=0.13
Requires-Dist: eth-utils>=5.0
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: mcp>=1.9
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Requires-Dist: sqlalchemy[asyncio]>=2.0
Requires-Dist: structlog>=24.1
Requires-Dist: tenacity>=8.3
Requires-Dist: uvicorn[standard]>=0.30
Requires-Dist: web3>=7.0
Description-Content-Type: text/markdown

# pontage

<p align="center">
  <img src="docs/assets/402-payment-required.png" alt="402 Payment Required" width="400">
</p>

**A self-settling [x402](https://www.x402.org/) payment gateway for MCP tools.**
Gate any [Model Context Protocol](https://modelcontextprotocol.io) tool behind a
per-call USDC micropayment — and verify and settle the payment **directly against
the chain**, with no third-party facilitator in the path.

License: MIT &nbsp;·&nbsp; Python 3.12 &nbsp;·&nbsp; Base + USDC (EIP-3009)

> **pontage** *(n.)* — the medieval toll paid to cross a bridge. The agent
> pays pontage; the gateway takes it straight across to the chain.

---

## Why

AI agents increasingly consume gated tools, APIs, and data with no human in the
loop to click "subscribe." [x402](https://www.x402.org/) revives HTTP 402 as the
protocol for this: the server answers `402 Payment Required`, the agent signs a
stablecoin payment, and the server serves the resource once paid.

The reference x402 deployments (Cloudflare, AWS) delegate payment
**verification and settlement** to a hosted facilitator. `pontage` collapses
that role into the server itself:

```
   reference x402:   Agent ──▶ Server ──▶ Facilitator ──▶ Blockchain
   pontage:          Agent ──▶ Gateway ─────────────────▶ Blockchain
```

The buyer signs an **EIP-3009 `transferWithAuthorization`** — a gasless
authorization that moves USDC **directly from the buyer's wallet to the seller's
receiving address**. The gateway only *verifies the signature* and *broadcasts
the authorization*. It never custodies buyer funds and never holds spendable
revenue. The single key it holds pays gas and holds dust — the entire drainable
surface (see [Custody model](#custody-model)).

## Why not just have the agent pay and hand us the txid?

That's the naive design — and it's exactly what the [attack harness](#the-attack-harness)
below exists to break. It looks simple until you actually build it:

- **A txid can be replayed.** Nothing stops an agent from handing you a txid
  from a payment it already redeemed against you (or from a totally different
  service). Without a server-issued, single-use nonce that you atomically
  claim, "here's a txid" is just a receipt you have no way to invalidate.
- **A raw transfer isn't bound to what it paid for.** A token transfer says
  "A sent B tokens" — nothing more. It doesn't say *which* tool call, *which*
  arguments, or *which* price it was authorizing. Without binding the payment
  to the exact request up front, an agent can pay the cheap price once and
  swap in a more expensive call before you check.
- **"I sent a txid" isn't "the money moved."** The transaction could still be
  pending, get dropped, get replaced, or fall out of the chain in a reorg.
  Serving as soon as you see a txid — instead of after on-chain confirmation —
  means you're trusting the agent's claim, which is the exact trust problem
  HTTP 402 exists to remove.
- **You need to check it against a live chain anyway.** Confirming a payment
  really landed means an RPC call (or your own node), decoding the receipt,
  confirmation-depth logic that scales with price, and idempotent handling so
  a slow confirmation doesn't get double-charged or double-served.
- **The agent still needs to learn the price and address first.** Even in the
  "just send a txid" version, there's an implicit handshake before payment —
  which is precisely what the 402 challenge formalizes instead of leaving
  ad hoc.

So the job doesn't go away — mint a challenge, bind it to the request, verify
the payment against it, atomically claim it exactly once, wait for on-chain
confirmation, *then* serve. `pontage`'s bet isn't that this job is avoidable;
it's that you don't need to hand it to a third-party facilitator to do it
correctly. Verification and settlement happen in-process, directly against
the chain.

## What makes it different

- **No facilitator dependency.** Verify + settle happen in-process over plain
  JSON-RPC. No hosted service, no third-party account, no facilitator economics.
- **A reproducible attack-test harness.** The paper
  [*Five Attacks on x402*](https://arxiv.org/abs/2605.11781) found every audited
  open-source x402 SDK exploitable. `pontage` ships each attack class as a
  runnable test that **succeeds against a naive gateway and fails against this
  one** — so you verify the defenses yourself instead of trusting a claim. See
  [`tests/attacks/`](tests/attacks/).
- **Payment bound to the exact request.** The gateway issues the EIP-3009 nonce
  and binds it to a specific tool + argument set, closing the request↔payment
  binding gap that underlies most x402 replay attacks.
- **Clean settlement-adapter interface.** The engine depends only on a
  `SettlementAdapter` protocol; the Base/USDC EIP-3009 implementation is one
  swappable module.

## Quick start

Requires [uv](https://docs.astral.sh/uv/) and Python 3.12.

```bash
uv sync

# Run the gateway with an in-memory fake chain (no wallet, no network needed).
PONTAGE_CHAIN_MODE=fake \
PONTAGE_PAY_TO_ADDRESS=0x2222222222222222222222222222222222222222 \
uv run python -m pontage
```

In another terminal, run the example paying agent end-to-end:

```bash
uv run python examples/client.py --tool lucky_number --args '{"seed": "pontage"}'
```

```
→ calling lucky_number (no payment)
← 402 challenge: $0.001 USDC on base-sepolia to 0x2222…2222
→ paying and re-calling lucky_number
← tool result: { "seed": "pontage", "luckyNumber": 37, ... }
← settlement receipt: { "success": true, "transaction": "0x0f3c…10ed", ... }
```

The client fetches the 402 challenge, signs the EIP-3009 authorization with the
server-issued nonce, and re-calls the tool with the payment in
`_meta["x402/payment"]` — the [x402 MCP transport](https://www.x402.org/)
pattern.

## Defining a paid tool

```python
from pontage.transport.paid_tool import paid_tool

@paid_tool(mcp, get_engine, price_atomic=1_000)  # $0.001 (USDC has 6 decimals)
async def market_lookup(symbol: str) -> dict:
    """Premium market data for a symbol."""
    return await fetch_quote(symbol)
```

The first call returns a `402`-style challenge; the paid retry runs the tool and
returns a settlement receipt. Payment travels in `_meta`, never in the tool
arguments — the tool's public schema stays clean.

## Payment flow (Mode A: confirm-before-serve)

1. Agent calls the tool with no payment.
2. Gateway records a challenge (server-issued nonce, price, recipient, expiry)
   and returns the x402 `402` challenge.
3. Agent signs an EIP-3009 authorization over the challenge's nonce and re-calls.
4. Gateway verifies the payload **against the stored challenge** (signature,
   recipient, amount, validity window, resource binding), atomically **claims the
   nonce** (DB unique constraint — the replay lock), then broadcasts the
   authorization and **waits for on-chain confirmation**.
5. Only after funds have moved does the tool run; the receipt (tx hash) is
   returned in `_meta["x402/payment-response"]`.

Serving is gated on *money moved*, not on signature validity — the core defense
against the "unpaid service" attack.

## Custody model

| Key | Held where | Can spend | Worst case if the gateway is fully compromised |
|-----|-----------|-----------|-----------------------------------------------|
| **Receiving address** | Cold wallet; gateway knows the *public address only* | Seller revenue | Cannot be drained via the gateway. At most, future challenges point at an attacker address — detectable (receipts stop matching) and reversible. |
| **Submitter key** | Gateway host (env / secret store) | Gas only — holds dust | Gas dust lost. Bounded by construction. |

Because EIP-3009 moves funds buyer → receiving address directly, the total
drainable surface is gas dust plus whatever revenue cap you configure
(`PONTAGE_FLOAT_CAP_ATOMIC`).

## Configuration

Environment variables, prefix `PONTAGE_`:

| Variable | Default | Meaning |
|----------|---------|---------|
| `PAY_TO_ADDRESS` | *(required)* | Receiving address (public only) |
| `CHAIN_MODE` | `rpc` | `rpc` (real chain) or `fake` (in-memory demo chain) |
| `CHAIN_ID` | `84532` | `8453` Base mainnet · `84532` Base Sepolia |
| `RPC_URL` | `https://sepolia.base.org` | JSON-RPC endpoint |
| `SUBMITTER_PRIVATE_KEY` | — | Gas-paying key (dust); required in `rpc` mode |
| `DATABASE_URL` | `sqlite+aiosqlite:///./pontage.db` | Postgres in production |
| `CONFIRMATION_THRESHOLD_ATOMIC` | `100000` | Prices ≥ this wait for more confirmations |
| `FLOAT_CAP_ATOMIC` | `50000000` | Refuse new challenges past this cumulative revenue ($50) |

See [`.env.example`](.env.example) and [`docs/`](docs/) for the full list.

## The attack harness

```bash
uv run pytest tests/attacks/ -v
```

Each test attacks both a deliberately-naive gateway and the real engine under
identical on-chain conditions (a real in-memory chain model, not a mock), and
asserts the exploit **succeeds against naive, fails against pontage**:

| Attack | Class | Defense |
|--------|-------|---------|
| 1 | Replay across HTTP/chain boundary | Atomic nonce claim (DB unique constraint) |
| 2 | Request/resource binding flaw | Server-issued nonce bound to tool + arguments |
| 3 | Authorization weaknesses | Every field checked against the challenge; signer recovered |
| 4 | Stale/expired authorization | Expiry + settlement-headroom checks |
| 5 | Settlement-path inconsistency | Confirm-before-serve + idempotent retry |

## Status & scope

`pontage` is **v0.1** — a working, demonstrable slice: one FastAPI service, an
MCP server with paid tools, EIP-3009 verify + settle on Base, and the attack
harness. It is a reference implementation, not a hardened financial system; run
the public demo on a capped mainnet float, and read the
[custody model](#custody-model) before pointing real money at it.

Deferred to later milestones: a Go settlement core, serve-on-verify (Mode B),
prepaid tabs, a plain-HTTP middleware surface, and additional chain adapters
(the interface stays generic).

## Development

```bash
uv sync
uv run ruff check src tests        # lint
uv run ruff format src tests       # format
uv run mypy src/pontage           # strict type check
uv run pytest                      # tests (no network needed)
```

## License

MIT — see [LICENSE](LICENSE).
