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.
- interline-0.1.0.dist-info/METADATA +82 -0
- interline-0.1.0.dist-info/RECORD +26 -0
- interline-0.1.0.dist-info/WHEEL +5 -0
- interline-0.1.0.dist-info/entry_points.txt +2 -0
- interline-0.1.0.dist-info/licenses/LICENSE +21 -0
- interline-0.1.0.dist-info/top_level.txt +2 -0
- mcp_router/README.md +63 -0
- mcp_router/__init__.py +1 -0
- mcp_router/discovery.py +152 -0
- mcp_router/selftest.py +144 -0
- mcp_router/server.py +85 -0
- router/__init__.py +0 -0
- router/buyer.py +70 -0
- router/config.py +113 -0
- router/facilitator_mock.py +119 -0
- router/facilitator_real.py +67 -0
- router/inbound/__init__.py +0 -0
- router/inbound/ap2_adapter.py +234 -0
- router/ledger.py +36 -0
- router/paywall.py +153 -0
- router/rails/__init__.py +61 -0
- router/rails/base.py +45 -0
- router/rails/mpp_rail.py +332 -0
- router/rails/x402_rail.py +73 -0
- router/seller.py +95 -0
- router/wallet.py +62 -0
|
@@ -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,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.
|
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)."""
|
mcp_router/discovery.py
ADDED
|
@@ -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
|