interline 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: interline
3
+ Version: 0.1.0
4
+ Summary: Neutral, non-custodial MCP server that lets any AI agent pay across payment rails (x402 today, more landing) through one integration — rail-discovery + routing + a unified receipt ledger.
5
+ License: MIT
6
+ Keywords: mcp,model-context-protocol,x402,agent-payments,payment-router,agentic-payments,non-custodial,usdc,ai-agent
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: mcp>=1.0
11
+ Requires-Dist: httpx>=0.28
12
+ Requires-Dist: x402>=2.13
13
+ Requires-Dist: eth-account>=0.13
14
+ Requires-Dist: web3>=7.0
15
+ Requires-Dist: fastapi>=0.110
16
+ Provides-Extra: server
17
+ Requires-Dist: uvicorn>=0.27; extra == "server"
18
+ Dynamic: license-file
19
+
20
+ # Interline (MCP server)
21
+
22
+ **Rail-discovery + future-proof payments for AI agents — as an MCP server.**
23
+
24
+ Two things your agent gets, **today**:
25
+ 1. **Discover** which payment rail(s) any paid endpoint accepts — *before* paying. (The discovery layer single-rail clients don't have.)
26
+ 2. **Pay** through it — non-custodial, your own key, capped.
27
+
28
+ And the part that pays off across **every rail**: **integrate once.** Three rails are live today — [x402](https://x402.org) (HTTP 402 + USDC) on EVM (Base Sepolia) and on Solana (devnet), plus **MPP** on Tempo (Moderato testnet) — all behind the *same tool*. Real on-chain agent-to-agent settles are confirmed on all three. Every future rail drops in with **zero code change**. **Never re-integrate agent payments again** — when the next rail ships, your agent already speaks it.
29
+
30
+ This is **not "another x402 MCP."** Single-rail clients make you wire up one rail (and re-wire for the next). This is the **discovery + routing layer above them** — same shape OpenRouter has for models. Non-custodial by design: payments use **your own wallet key**; this server never holds, sees, or routes funds through itself.
31
+
32
+ ## Tools
33
+
34
+ | Tool | What it does |
35
+ |---|---|
36
+ | `discover_payment_rails(url)` | Probe a paid endpoint; report **which rails + prices** it accepts. **No payment.** The discovery layer. |
37
+ | `pay_for_resource(url, task, max_price_usdc)` | Pay + fetch through the accepted rail; return content **+ a settlement receipt**. Never exceeds `max_price_usdc`. |
38
+ | `payment_history(limit)` | The **unified cross-rail receipt ledger** — every settlement, every rail, one view. |
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install interline # (or: uvx interline)
44
+ ```
45
+
46
+ ### Add to Claude Desktop / Cursor / any MCP client
47
+
48
+ ```jsonc
49
+ {
50
+ "mcpServers": {
51
+ "interline": {
52
+ "command": "uvx",
53
+ "args": ["interline"],
54
+ "env": {
55
+ "APV0_BUYER_PRIVATE_KEY": "0x...", // YOUR wallet key — stays in your env, never leaves your machine
56
+ "APV0_NETWORK": "base-sepolia" // or "base" for mainnet
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Local dev (from a clone):
64
+
65
+ ```jsonc
66
+ { "mcpServers": { "interline": {
67
+ "command": "python", "args": ["-m", "mcp_router.server"],
68
+ "cwd": "/path/to/interline-routes",
69
+ "env": { "APV0_BUYER_PRIVATE_KEY": "0x...", "APV0_NETWORK": "base-sepolia" }
70
+ }}}
71
+ ```
72
+
73
+ `discover_payment_rails` needs no key (it only reads the 402 challenge). `pay_for_resource` needs `APV0_BUYER_PRIVATE_KEY` (your funded wallet).
74
+
75
+ ## Non-custodial guarantee
76
+
77
+ - The router **never holds funds.** `discover` only reads a public 402 challenge; `pay` signs with **your** key, locally, bounded by `max_price_usdc`.
78
+ - The fee model (when one exists) is a **software/routing fee billed to you, the developer — never a cut of the funds flow.** That's the line between software (this) and money transmission (not this).
79
+
80
+ ## Status
81
+
82
+ Three rails live — x402 USDC on EVM (Base Sepolia) and on Solana (devnet), plus MPP on Tempo (Moderato testnet). Real on-chain agent-to-agent settles confirmed on all three. Neutral by construction: `discover_payment_rails` surfaces *any* rail an endpoint offers, and each additional rail is a drop-in adapter with no caller change. Built on the [interline-routes](https://github.com/Choppaaahh/interline-routes) rail-agnostic core.
@@ -0,0 +1,26 @@
1
+ interline-0.1.0.dist-info/licenses/LICENSE,sha256=lEOBkHaBtq9dL_66eHTx8uHWTOTA9ldZzSdmyOweYrs,1067
2
+ mcp_router/README.md,sha256=uwE4iBuBJ5OJSEkvaWO9yBj6DcLtZrNCj7lYq9qFtBk,3325
3
+ mcp_router/__init__.py,sha256=tfBBqEciI85AfxELWQ_utGAWeS6VIrkVQ0e9K1JanQ0,101
4
+ mcp_router/discovery.py,sha256=E3LYz020e3J8-RL7kMXvegd1kfEeubpeyqVE9V2E-a4,7276
5
+ mcp_router/selftest.py,sha256=-5T1nC_Q8ru9VrRzmepv5xHivwMQACDXo_Hom2qsvBw,6605
6
+ mcp_router/server.py,sha256=lEJlJMa8rhHLU5fXpY6wnjBNEMjbGCNGR1nN_eI9L6U,3628
7
+ router/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ router/buyer.py,sha256=RTUP0yiAkhhyeSPAcxMP2tHzVZ0eC-aWjUkWJ1RQxrg,2994
9
+ router/config.py,sha256=kIV3YA4EUhGezFULV5vbCKbSNeEYLhik8YANFasafZM,4824
10
+ router/facilitator_mock.py,sha256=_ZiysMS1YLTSFVv3qKPuW0VR8LsScMSjRtfNvOHjOB8,5523
11
+ router/facilitator_real.py,sha256=KmEbK31LbZHsj5uPGf0jEWs5ajvmyab28VD6hDPyS7I,3057
12
+ router/ledger.py,sha256=Zw0MM9tknVF07CPGrx-hxPMm2OIBeucVuqfOvw7iHuA,1134
13
+ router/paywall.py,sha256=R-z_CygkGUdzZGR9ibPSTEkgev9oyGuiJcgtMBQ1HAY,7781
14
+ router/seller.py,sha256=U1UXdDlIRxMIZTyL28Y0bkaOzkk_WjE1GKLnm2EO_uE,3645
15
+ router/wallet.py,sha256=XGjGo6UeIyGzvUtP-cyj-yupNGilAcpaxq-y1PHF8tY,2695
16
+ router/inbound/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ router/inbound/ap2_adapter.py,sha256=lYTJIG-a0IBniYMc1Fu_gXSFoCxZ0CuQgAcBA105bzM,10888
18
+ router/rails/__init__.py,sha256=GUGMvk94xKRsOX60HpHE9JNyuyEQj6mWzyDHK326gbU,1791
19
+ router/rails/base.py,sha256=xIrosBQAwhnETL4OlIIv2qHKS3SlaP4tcDRW6lbQaLk,1934
20
+ router/rails/mpp_rail.py,sha256=2pFZmcNZopilePdOeOwwiVX8K7XENRaKC-sECxFMJOw,14923
21
+ router/rails/x402_rail.py,sha256=4_tbHtrtMTNd-vqYEoJk2Kbz3PQ16XgviefbPGRvt-g,3220
22
+ interline-0.1.0.dist-info/METADATA,sha256=RMV6gME4PwrB7nNT7O9ciftPiM9zxj9Dkr_b2MHRVQI,4052
23
+ interline-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
24
+ interline-0.1.0.dist-info/entry_points.txt,sha256=hJ_eY4McNsKkhFEWbYezz2tGwKASbnGeMgI_Uh8UONE,53
25
+ interline-0.1.0.dist-info/top_level.txt,sha256=VFm1NnWafoTx7THzUzVdKx1ZfZo0FsA6VLx-q4RzVzg,18
26
+ interline-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ interline = mcp_router.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Choppaaahh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ mcp_router
2
+ router
mcp_router/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Interline (MCP server)
2
+
3
+ **Rail-discovery + future-proof payments for AI agents — as an MCP server.**
4
+
5
+ Two things your agent gets, **today**:
6
+ 1. **Discover** which payment rail(s) any paid endpoint accepts — *before* paying. (The discovery layer single-rail clients don't have.)
7
+ 2. **Pay** through it — non-custodial, your own key, capped.
8
+
9
+ And the part that pays off across **every rail**: **integrate once.** Three rails are live today — [x402](https://x402.org) (HTTP 402 + USDC) on EVM (Base Sepolia) and on Solana (devnet), plus **MPP** on Tempo (Moderato testnet) — all behind the *same tool*. Real on-chain agent-to-agent settles are confirmed on all three. Every future rail drops in with **zero code change**. **Never re-integrate agent payments again** — when the next rail ships, your agent already speaks it.
10
+
11
+ This is **not "another x402 MCP."** Single-rail clients make you wire up one rail (and re-wire for the next). This is the **discovery + routing layer above them** — same shape OpenRouter has for models. Non-custodial by design: payments use **your own wallet key**; this server never holds, sees, or routes funds through itself.
12
+
13
+ ## Tools
14
+
15
+ | Tool | What it does |
16
+ |---|---|
17
+ | `discover_payment_rails(url)` | Probe a paid endpoint; report **which rails + prices** it accepts. **No payment.** The discovery layer. |
18
+ | `pay_for_resource(url, task, max_price_usdc)` | Pay + fetch through the accepted rail; return content **+ a settlement receipt**. Never exceeds `max_price_usdc`. |
19
+ | `payment_history(limit)` | The **unified cross-rail receipt ledger** — every settlement, every rail, one view. |
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install interline # (or: uvx interline)
25
+ ```
26
+
27
+ ### Add to Claude Desktop / Cursor / any MCP client
28
+
29
+ ```jsonc
30
+ {
31
+ "mcpServers": {
32
+ "interline": {
33
+ "command": "uvx",
34
+ "args": ["interline"],
35
+ "env": {
36
+ "APV0_BUYER_PRIVATE_KEY": "0x...", // YOUR wallet key — stays in your env, never leaves your machine
37
+ "APV0_NETWORK": "base-sepolia" // or "base" for mainnet
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Local dev (from a clone):
45
+
46
+ ```jsonc
47
+ { "mcpServers": { "interline": {
48
+ "command": "python", "args": ["-m", "mcp_router.server"],
49
+ "cwd": "/path/to/interline-routes",
50
+ "env": { "APV0_BUYER_PRIVATE_KEY": "0x...", "APV0_NETWORK": "base-sepolia" }
51
+ }}}
52
+ ```
53
+
54
+ `discover_payment_rails` needs no key (it only reads the 402 challenge). `pay_for_resource` needs `APV0_BUYER_PRIVATE_KEY` (your funded wallet).
55
+
56
+ ## Non-custodial guarantee
57
+
58
+ - The router **never holds funds.** `discover` only reads a public 402 challenge; `pay` signs with **your** key, locally, bounded by `max_price_usdc`.
59
+ - The fee model (when one exists) is a **software/routing fee billed to you, the developer — never a cut of the funds flow.** That's the line between software (this) and money transmission (not this).
60
+
61
+ ## Status
62
+
63
+ Three rails live — x402 USDC on EVM (Base Sepolia) and on Solana (devnet), plus MPP on Tempo (Moderato testnet). Real on-chain agent-to-agent settles confirmed on all three. Neutral by construction: `discover_payment_rails` surfaces *any* rail an endpoint offers, and each additional rail is a drop-in adapter with no caller change. Built on the [interline-routes](https://github.com/Choppaaahh/interline-routes) rail-agnostic core.
mcp_router/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Interline — MCP server: let any MCP agent pay across payment rails (neutral, non-custodial)."""
@@ -0,0 +1,152 @@
1
+ """
2
+ Rail discovery — given a paid endpoint, report which payment rails it accepts.
3
+
4
+ THIS is the differentiator vs "another x402 payment MCP": one call tells an agent
5
+ *every* way it could pay an endpoint, across whatever rails the endpoint offers —
6
+ the neutral router's rail-discovery layer. Today x402 is live; as rails land (MPP, …)
7
+ they surface in the same shape with zero caller change.
8
+
9
+ The parse step (`parse_accepts`) is split from the network fetch (`discover_rails`) so
10
+ the normalization is golden-fixture testable with no network.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import httpx
15
+
16
+ # x402/MPP payment `scheme` → human rail name. Grows as rails land.
17
+ # Unknown schemes fall through to themselves, so discovery still SURFACES a rail we don't
18
+ # yet have a pretty name for (neutrality: report what's offered, don't hide it).
19
+ SCHEME_TO_RAIL = {"exact": "x402", "mpp": "mpp"}
20
+
21
+ # Default token decimals when not derivable from the challenge (USDC = 6).
22
+ _DEFAULT_DECIMALS = 6
23
+
24
+
25
+ def parse_accepts(body: dict) -> list[dict]:
26
+ """Pure: normalize an x402 402-challenge body's `accepts` array into a rail list."""
27
+ rails: list[dict] = []
28
+ for a in (body.get("accepts") or []):
29
+ scheme = a.get("scheme", "?")
30
+ extra = a.get("extra") or {}
31
+ amt = a.get("amount")
32
+ try:
33
+ human = f"{int(amt) / 10 ** _DEFAULT_DECIMALS:.6f} {extra.get('name', 'token')}"
34
+ except (TypeError, ValueError):
35
+ human = str(amt)
36
+ rails.append({
37
+ "rail": SCHEME_TO_RAIL.get(scheme, scheme),
38
+ "scheme": scheme,
39
+ "network": a.get("network"),
40
+ "asset": a.get("asset"),
41
+ "amount_atomic": amt,
42
+ "price": human,
43
+ "pay_to": a.get("payTo"),
44
+ })
45
+ return rails
46
+
47
+
48
+ def discover_rails(url: str, timeout: float = 15.0) -> dict:
49
+ """GET `url`; if it returns a 402 challenge, report the rails it accepts. NO payment is made."""
50
+ try:
51
+ r = httpx.get(url, timeout=timeout, follow_redirects=True)
52
+ except Exception as e: # noqa: BLE001 — surface the failure as data, don't raise into the agent
53
+ return {"url": url, "error": f"fetch failed: {e}", "rails": []}
54
+ if r.status_code != 402:
55
+ return {
56
+ "url": url, "paid": False, "status_code": r.status_code,
57
+ "note": "no 402 payment challenge (endpoint is free, or not an x402 resource)",
58
+ "rails": [],
59
+ }
60
+ try:
61
+ body = r.json()
62
+ except Exception: # noqa: BLE001
63
+ return {"url": url, "paid": True, "error": "402 body was not JSON", "rails": []}
64
+ rails = parse_accepts(body)
65
+ return {
66
+ "url": url, "paid": True, "x402_version": body.get("x402Version"),
67
+ "rail_count": len(rails), "rails": rails,
68
+ }
69
+
70
+
71
+ # ── known-rails capability registry ──────────────────────────────────────────
72
+ # The neutral router is legible about the WHOLE landscape — including rails it does
73
+ # NOT settle itself. `route_mode` is the honesty knob:
74
+ # native-settle = Interline settles this directly (x402) or via an inbound adapter
75
+ # that lands on x402 (AP2). We move the funds.
76
+ # handoff = Interline recognizes the protocol + routes an agent TO it, but does
77
+ # NOT settle it — the protocol settles in its own world (Virtuals' own
78
+ # x402 escrow; OpenAI/Stripe ACP's card-only delegated payment).
79
+ KNOWN_RAILS = [
80
+ {
81
+ "name": "x402",
82
+ "kind": "settlement-rail",
83
+ "route_mode": "native-settle",
84
+ "networks": ["eip155 (EVM)", "solana (SVM)"],
85
+ "settle_asset": "USDC",
86
+ "what": "HTTP-402 micropayments. Interline settles natively across EVM + Solana behind one Paywall.",
87
+ "docs": "https://x402.org",
88
+ },
89
+ {
90
+ "name": "mpp",
91
+ "kind": "settlement-rail",
92
+ "route_mode": "native-settle",
93
+ "networks": ["tempo (stablecoin)"],
94
+ "settle_asset": "stablecoin",
95
+ "what": "Machine Payments Protocol (Stripe + Tempo, IETF draft-ryan-httpauth-payment). HTTP-402 "
96
+ "challenge/credential/receipt — convergent with x402, RFC-7235 framed. Interline settles it as "
97
+ "a second PROTOCOL behind the same Paywall (the cross-protocol wedge: pay an endpoint via x402 OR "
98
+ "mpp through one integration). Phase-1 runs the mock facilitator; live Tempo settle (official "
99
+ "pympp SDK) is gated on a funded Tempo wallet — no Stripe account required.",
100
+ "docs": "https://mpp.dev",
101
+ },
102
+ {
103
+ "name": "ap2",
104
+ "kind": "authorization-layer",
105
+ "route_mode": "native-settle",
106
+ "networks": ["eip155 (EVM)", "solana (SVM)"],
107
+ "settle_asset": "USDC",
108
+ "what": "Google Agent Payments Protocol — signed SD-JWT mandates. Interline's AP2 inbound adapter "
109
+ "verifies the mandate + constraints + freshness, then settles on x402. One seam for the "
110
+ "card/agent-commerce tier (UCP / Mastercard / Amex / PayPal all delegate to AP2).",
111
+ "docs": "https://github.com/google-agentic-commerce/AP2",
112
+ },
113
+ {
114
+ "name": "virtuals-acp",
115
+ "kind": "commerce-ecosystem",
116
+ "route_mode": "handoff",
117
+ "networks": ["eip155:8453 (Base)"],
118
+ "settle_asset": "USDC",
119
+ "what": "Virtuals Protocol Agent Commerce Protocol — a crypto-native agent marketplace running its OWN "
120
+ "x402 rail + on-chain escrow on Base. Interline routes an agent to it (handoff); it settles in "
121
+ "its own ecosystem, not ours. Python SDK: virtuals-acp.",
122
+ "docs": "https://whitepaper.virtuals.io/about-virtuals/agent-commerce-protocol-acp",
123
+ },
124
+ {
125
+ "name": "openai-stripe-acp",
126
+ "kind": "commerce-layer",
127
+ "route_mode": "handoff",
128
+ "networks": ["card / PSP networks"],
129
+ "settle_asset": "fiat (card)",
130
+ "what": "OpenAI/Stripe Agentic Commerce Protocol (ChatGPT Instant Checkout). Its delegated payment is "
131
+ "CARD-ONLY (payment_method_type=card, settled through PSP/card networks), so Interline routes an "
132
+ "agent to it (handoff) — our crypto rail can't be the settlement target.",
133
+ "docs": "https://github.com/agentic-commerce-protocol/agentic-commerce-protocol",
134
+ },
135
+ ]
136
+
137
+
138
+ def known_rails_catalog() -> dict:
139
+ """The neutral router's full rail catalog — what Interline knows about + how it relates to each.
140
+
141
+ Separates rails Interline SETTLES natively (route_mode=native-settle: x402, AP2-via-adapter) from
142
+ protocols it ROUTES an agent to but does not settle (route_mode=handoff: Virtuals' own x402 escrow,
143
+ OpenAI/Stripe ACP's card-only delegated payment). Legibility over the whole landscape — reporting
144
+ every rail, including ones we don't move funds on — IS the neutral-router thesis."""
145
+ settles = [r["name"] for r in KNOWN_RAILS if r["route_mode"] == "native-settle"]
146
+ handoffs = [r["name"] for r in KNOWN_RAILS if r["route_mode"] == "handoff"]
147
+ return {
148
+ "rail_count": len(KNOWN_RAILS),
149
+ "native_settle": settles,
150
+ "handoff": handoffs,
151
+ "rails": KNOWN_RAILS,
152
+ }
mcp_router/selftest.py ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP-router self-test — NO network, NO keys. Golden-fixture verified.
4
+
5
+ Proves:
6
+ 1. parse_accepts normalizes a single-rail x402 402 body (golden: 1000 atomic -> "0.001000 USDC").
7
+ 2. parse_accepts is NEUTRAL — surfaces a SECOND, unknown-scheme rail (proves discovery isn't x402-only).
8
+ 3. parse_accepts on empty/no-accepts -> 0 rails (no crash).
9
+ 4. discover_rails routes: 402 -> rails parsed; 200 -> paid=False; fetch-error -> error surfaced.
10
+ 5. payment_history reads the ledger shape.
11
+
12
+ Run from repo root: python -m mcp_router.selftest
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20
+
21
+ from mcp_router import discovery # noqa: E402
22
+
23
+ FAILS = []
24
+
25
+
26
+ def check(name, cond):
27
+ print(f" {'PASS' if cond else 'FAIL'} {name}")
28
+ if not cond:
29
+ FAILS.append(name)
30
+
31
+
32
+ # --- GOLDEN FIXTURES (CC-computed, asserted verbatim — model must not recompute) ---
33
+ # single x402 rail: amount 1000 atomic / 1e6 = 0.001 USDC
34
+ GOLDEN_SINGLE = {
35
+ "x402Version": 2,
36
+ "accepts": [
37
+ {"scheme": "exact", "network": "eip155:84532", "asset": "0xUSDC",
38
+ "amount": "1000", "payTo": "0xSELLER", "extra": {"name": "USDC"}},
39
+ ],
40
+ }
41
+ # multi-rail: x402 (1000 -> 0.001) + an unknown "mpp" scheme (5000 -> 0.005)
42
+ GOLDEN_MULTI = {
43
+ "x402Version": 2,
44
+ "accepts": [
45
+ {"scheme": "exact", "network": "eip155:84532", "amount": "1000", "payTo": "0xA", "extra": {"name": "USDC"}},
46
+ {"scheme": "mpp", "network": "eip155:8453", "amount": "5000", "payTo": "0xB", "extra": {"name": "USDC"}},
47
+ ],
48
+ }
49
+
50
+
51
+ class FakeResp:
52
+ def __init__(self, status, payload=None, raise_json=False):
53
+ self.status_code = status
54
+ self._payload = payload
55
+ self._raise = raise_json
56
+
57
+ def json(self):
58
+ if self._raise:
59
+ raise ValueError("not json")
60
+ return self._payload
61
+
62
+
63
+ def main():
64
+ # 1. single-rail golden
65
+ rails = discovery.parse_accepts(GOLDEN_SINGLE)
66
+ check("single-rail -> 1 rail", len(rails) == 1)
67
+ check("single-rail scheme exact -> rail x402", rails[0]["rail"] == "x402")
68
+ check("single-rail price = 0.001000 USDC (golden)", rails[0]["price"] == "0.001000 USDC")
69
+ check("single-rail pay_to passthrough", rails[0]["pay_to"] == "0xSELLER")
70
+ check("single-rail amount_atomic passthrough", rails[0]["amount_atomic"] == "1000")
71
+
72
+ # 2. multi-rail neutrality — unknown scheme surfaced as its own rail
73
+ rails = discovery.parse_accepts(GOLDEN_MULTI)
74
+ check("multi-rail -> 2 rails", len(rails) == 2)
75
+ check("multi-rail surfaces unknown 'mpp' scheme as rail", rails[1]["rail"] == "mpp")
76
+ check("multi-rail mpp price = 0.005000 USDC (golden)", rails[1]["price"] == "0.005000 USDC")
77
+ check("multi-rail networks distinct", rails[0]["network"] != rails[1]["network"])
78
+
79
+ # 3. empty / malformed
80
+ check("no-accepts -> 0 rails", discovery.parse_accepts({}) == [])
81
+ check("null-accepts -> 0 rails", discovery.parse_accepts({"accepts": None}) == [])
82
+ bad_amt = discovery.parse_accepts({"accepts": [{"scheme": "exact", "amount": None}]})
83
+ check("None amount -> price is string, no crash", bad_amt[0]["price"] == "None")
84
+
85
+ # 4. discover_rails routing (monkeypatch httpx.get)
86
+ orig = discovery.httpx.get
87
+ try:
88
+ discovery.httpx.get = lambda url, **kw: FakeResp(402, GOLDEN_SINGLE)
89
+ d = discovery.discover_rails("http://x/work")
90
+ check("402 -> paid True + 1 rail", d.get("paid") is True and d.get("rail_count") == 1)
91
+
92
+ discovery.httpx.get = lambda url, **kw: FakeResp(200, {"work": "free"})
93
+ d = discovery.discover_rails("http://x/free")
94
+ check("200 -> paid False", d.get("paid") is False and d.get("rails") == [])
95
+
96
+ def _raise(url, **kw):
97
+ raise RuntimeError("conn refused")
98
+ discovery.httpx.get = _raise
99
+ d = discovery.discover_rails("http://x/down")
100
+ check("fetch error -> error surfaced as data", "error" in d and d["rails"] == [])
101
+
102
+ discovery.httpx.get = lambda url, **kw: FakeResp(402, None, raise_json=True)
103
+ d = discovery.discover_rails("http://x/badjson")
104
+ check("402 non-JSON -> error surfaced", "error" in d)
105
+ finally:
106
+ discovery.httpx.get = orig
107
+
108
+ # 5. payment_history shape (import server lazily; ledger may be empty)
109
+ from mcp_router import server # noqa: F401
110
+ hist = server.payment_history(limit=5)
111
+ check("payment_history returns count+settlements", "count" in hist and "settlements" in hist)
112
+
113
+ # 6. known-rails catalog (GOLDEN — neutral landscape, settle-vs-handoff honesty)
114
+ cat = discovery.known_rails_catalog()
115
+ check("catalog has 5 known rails", cat["rail_count"] == 5)
116
+ check("native_settle = [x402, mpp, ap2] (golden)", cat["native_settle"] == ["x402", "mpp", "ap2"])
117
+ check("handoff = [virtuals-acp, openai-stripe-acp] (golden)",
118
+ cat["handoff"] == ["virtuals-acp", "openai-stripe-acp"])
119
+ by = {r["name"]: r for r in cat["rails"]}
120
+ check("x402 is native-settle", by["x402"]["route_mode"] == "native-settle")
121
+ check("mpp is native-settle (the cross-protocol wedge)", by["mpp"]["route_mode"] == "native-settle")
122
+ check("mpp is a settlement-rail (a 2nd protocol, not an authz layer)", by["mpp"]["kind"] == "settlement-rail")
123
+ check("ap2 is native-settle", by["ap2"]["route_mode"] == "native-settle")
124
+ check("virtuals-acp is handoff (its own x402 escrow)", by["virtuals-acp"]["route_mode"] == "handoff")
125
+ # honesty assertion: ACP is card-only -> handoff, NOT a crypto rail we settle
126
+ check("openai-stripe-acp is handoff", by["openai-stripe-acp"]["route_mode"] == "handoff")
127
+ check("openai-stripe-acp settle_asset is fiat/card (NOT crypto — honesty)",
128
+ "card" in by["openai-stripe-acp"]["settle_asset"].lower() or "fiat" in by["openai-stripe-acp"]["settle_asset"].lower())
129
+ required = {"name", "kind", "route_mode", "networks", "settle_asset", "what", "docs"}
130
+ check("every rail has full metadata", all(required <= set(r) for r in cat["rails"]))
131
+ check("every route_mode is native-settle or handoff",
132
+ all(r["route_mode"] in ("native-settle", "handoff") for r in cat["rails"]))
133
+ # the MCP tool returns the same catalog
134
+ check("server.list_known_rails tool == catalog", server.list_known_rails() == cat)
135
+
136
+ print()
137
+ if FAILS:
138
+ print(f"SELF-TEST FAIL: {len(FAILS)} -> {FAILS}")
139
+ sys.exit(1)
140
+ print("SELF-TEST PASS — MCP router discovery (neutral multi-rail) + history wired, golden-fixture verified.")
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
mcp_router/server.py ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Interline — an MCP server that lets ANY MCP-using agent pay across payment rails.
4
+
5
+ Three tools:
6
+ discover_payment_rails(url) — probe a paid endpoint; report which rails + prices it accepts (NO payment)
7
+ pay_for_resource(url, task, max_usd) — pay + fetch through the accepted rail; return content + receipt
8
+ payment_history(limit) — the unified cross-rail receipt ledger
9
+
10
+ NEUTRAL by design: discovery reports EVERY rail an endpoint offers (today x402; MPP/others drop
11
+ into the same shape). NON-CUSTODIAL: payment uses the CALLER'S OWN wallet key (env), never ours.
12
+
13
+ Run as a stdio MCP server: python -m mcp_router.server (or the `interline` entry point)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ # Make `router` importable whether run from the repo root or as an installed entry point.
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
23
+
24
+ from mcp.server.fastmcp import FastMCP # noqa: E402
25
+ from mcp_router.discovery import discover_rails, known_rails_catalog # noqa: E402
26
+
27
+ mcp = FastMCP("interline")
28
+
29
+
30
+ @mcp.tool()
31
+ def discover_payment_rails(url: str) -> dict:
32
+ """Probe a paid endpoint and report which payment rails it accepts + the price for each — WITHOUT paying.
33
+
34
+ The neutral rail-discovery layer: one call tells your agent every way it could pay this endpoint,
35
+ across whatever rails the endpoint offers. Use this before pay_for_resource to see the price/rails."""
36
+ return discover_rails(url)
37
+
38
+
39
+ @mcp.tool()
40
+ def pay_for_resource(url: str, task: str = "", max_price_usdc: float = 0.01) -> dict:
41
+ """Pay for and fetch a paywalled resource, returning its content + a settlement receipt.
42
+
43
+ Routes the payment through the rail the endpoint accepts; never pays more than max_price_usdc.
44
+ Uses the caller's OWN wallet key from APV0_BUYER_PRIVATE_KEY (non-custodial — this server never
45
+ holds or sees funds). `task` is appended as a query param for endpoints that take one."""
46
+ from router import buyer # local import: only needed on the pay path
47
+
48
+ max_atomic = int(round(max_price_usdc * 10 ** 6))
49
+ full_url = url if not task else f"{url}{'&' if '?' in url else '?'}task={task}"
50
+ try:
51
+ result = buyer.pay_and_get(full_url, max_price_atomic=max_atomic)
52
+ return {"ok": True, **result}
53
+ except Exception as e: # noqa: BLE001
54
+ return {"ok": False, "error": str(e)}
55
+
56
+
57
+ @mcp.tool()
58
+ def list_known_rails() -> dict:
59
+ """List EVERY agent-payment rail/protocol Interline knows about + how it relates to each — the neutral catalog.
60
+
61
+ Separates rails Interline SETTLES natively (route_mode=native-settle: x402, AP2-via-adapter) from protocols
62
+ it ROUTES you TO but does not settle (route_mode=handoff: Virtuals ACP's own on-chain escrow, OpenAI/Stripe
63
+ ACP's card-only delegated payment). One call = the whole agent-payment landscape, including the rails we
64
+ don't move funds on. Pair with discover_payment_rails (what a specific endpoint accepts)."""
65
+ return known_rails_catalog()
66
+
67
+
68
+ @mcp.tool()
69
+ def payment_history(limit: int = 20) -> dict:
70
+ """Return the most recent cross-rail settlement receipts — the unified ledger across every rail paid through."""
71
+ from router import ledger # local import
72
+
73
+ p = ledger.LEDGER
74
+ if not p.exists():
75
+ return {"count": 0, "settlements": []}
76
+ rows = [json.loads(ln) for ln in p.read_text().splitlines() if ln.strip()]
77
+ return {"count": len(rows), "settlements": rows[-limit:]}
78
+
79
+
80
+ def main() -> None:
81
+ mcp.run()
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
router/__init__.py ADDED
File without changes