Neutral micropayment router for autonomous agents — a single HTTP client across x402, L402, and MPP.
Project description
routeweiler
The neutral micropayment router for autonomous agents. A single async HTTP client —
await routeweiler.get(url) — that transparently handles 402 Payment Required across
x402 (EVM), L402 (Lightning), MPP-Tempo (stablecoin), and MPP-SPT (Stripe).
Install
pip install routeweiler
Python 3.11+ required.
Quick start
import asyncio
import os
from eth_account import Account
from routeweiler import Routeweiler, Funding
signer = Account.from_key(os.environ["PRIVATE_KEY"])
async def main():
async with Routeweiler(funding=[Funding.base_usdc(wallet=signer)]) as client:
response = await client.get("https://api.example.com/data")
print(response.json())
asyncio.run(main())
Supported rails
| Rail | Method | Funding source | Networks |
|---|---|---|---|
| x402 | EVM signed transaction | EvmFundingSource |
Base, Base-Sepolia |
| L402 | BOLT-11 Lightning invoice | LightningFundingSource |
Bitcoin, Regtest |
| MPP-Tempo | Tempo 0x76 stablecoin tx | TempoFundingSource |
Moderato testnet |
| MPP-SPT | Stripe Shared Payment Token | StripeFundingSource |
USD, EUR, GBP |
Funding sources
Each rail accepts a typed FundingSource holding the credentials it uses to sign or authorise payments. Key material stays in your process; Routeweiler never transmits private keys.
x402 — EvmFundingSource
An eth_account.LocalAccount that signs EIP-3009 transferWithAuthorization for USDC on Base.
from eth_account import Account
from routeweiler import Funding
wallet = Account.from_key(os.environ["PRIVATE_KEY"]) # 64-char hex secp256k1 key, 0x prefix optional
funding = Funding.base_usdc(wallet=wallet) # mainnet (chain 8453)
# funding = Funding.base_sepolia_usdc(wallet=wallet) # testnet (chain 84532)
L402 — LightningFundingSource
Wraps an LND gRPC client. Bring a running LND node and pass its admin macaroon + TLS cert; Routeweiler pays BOLT-11 invoices through it.
from routeweiler.funding.lightning import LightningFundingSource, LndClient
lnd = LndClient(
grpc_host="localhost",
grpc_port=10009,
macaroon_path="/path/to/admin.macaroon",
tls_cert_path="/path/to/tls.cert",
)
funding = await LightningFundingSource.create(lnd, network="bitcoin")
# network: "bitcoin" | "bitcoin-testnet" | "bitcoin-regtest" | "bitcoin-signet"
MPP-Tempo — TempoFundingSource
Signs Tempo's type-0x76 charge transactions with an eth_account.LocalAccount — same key shape as x402, different chain. The same wallet can fund both rails.
from eth_account import Account
from routeweiler import Funding
wallet = Account.from_key(os.environ["PRIVATE_KEY"]) # 64-char hex secp256k1 key, 0x prefix optional
funding = Funding.tempo_usdc(wallet=wallet) # mainnet, USDC
# funding = Funding.tempo_pathusd_moderato(wallet=wallet) # testnet, pathUSD
MPP-SPT — StripeFundingSource
No on-device signing. Stripe holds the card; you supply a secret API key, a customer id, and a saved payment method id. Routeweiler asks Stripe to mint a Shared Payment Token at pay-time.
from routeweiler import Funding
funding = Funding.stripe(
api_key=os.environ["STRIPE_API_KEY"], # sk_live_... / sk_test_...
customer="cus_ABC123", # buyer's Stripe customer id
payment_method="pm_XYZ789", # saved card / bank
currency="usd", # ISO-4217: usd | eur | gbp | ...
)
Pass any combination to Routeweiler(funding=[...]). The router picks the best rail per challenge based on Policy and what the server accepts.
SQLite trace recorder
Enable local tracing with TraceSink.sqlite. Every call (paid or free) produces
exactly one TraceEvent row, including the on-chain tx hash and the payment outcome:
from routeweiler import Routeweiler, Funding, TraceSink
async with Routeweiler(
funding=[Funding.base_usdc(wallet=signer)],
trace_sink=TraceSink.sqlite("./routeweiler.db"),
) as client:
response = await client.get("https://api.example.com/data")
# Inspect with the sqlite3 CLI:
# sqlite3 ./routeweiler.db \
# 'SELECT request_id, selected_rail, http_status FROM trace_events;'
Budget envelopes
Enforce per-session or per-agent spend caps with local SQLite budget envelopes.
Without a budget_envelope, tracing still works but no cap is enforced.
from routeweiler import BudgetEnvelope, Funding, Routeweiler, TraceSink
async with Routeweiler(
funding=[Funding.base_usdc(wallet=signer)],
trace_sink=TraceSink.sqlite("routeweiler.db"),
budget_envelope=BudgetEnvelope(
id="session-abc",
cap_minor_units=500, # 5.00 USD (in cents)
cap_currency="usd",
allowed_rails=["x402", "l402"],
ttl_seconds=3_600, # 1 hour
),
) as client:
response = await client.get("https://api.example.com/data")
budget_envelope accepts three forms:
None(default) — no cap enforcement.str— ID of a pre-existing envelope; raisesEnvelopeNotFoundErrorat construction time if the row is missing.BudgetEnvelope— declarative spec; If an envelope with the sameidalready exists it is reused unchanged.
Envelopes track reserved and settled amounts with Ed25519-signed draw receipts.
BudgetExceededError is raised if a payment would breach the cap.
Policy
Control which rails are used, set per-call spend limits, or deny specific URLs:
from routeweiler import Policy, PolicyRule, RuleMatch, Routeweiler
async with Routeweiler(
funding=[...],
policy=Policy(
currency="usd", # reference currency for max_per_call_minor_units
rules=[
PolicyRule(
name="deny analytics",
when=RuleMatch(url_matches="*.tracking.io"),
deny=True,
),
PolicyRule(
name="cap per call",
when=RuleMatch(url_matches="*"),
max_per_call_minor_units=500, # 5 USD cents
),
]
),
) as client:
...
max_per_call_minor_units requires a reference currency to compare rail-native quotes
against. Set Policy(currency="usd") (or any supported currency) when no
budget_envelope is configured. The envelope's cap_currency takes precedence when
both are present. If neither is provided and a rule uses max_per_call_minor_units,
Routeweiler raises ValueError at construction time.
Releases
Releases follow SemVer. Pre-1.0 minors (0.1.0 → 0.2.0) may include breaking changes.
| Tag format | Channel | Install |
|---|---|---|
v0.2.0 |
Stable | pip install routeweiler |
v0.2.0b1 |
Beta | pip install --pre routeweiler |
A release is a git tag, not a merge. Merges to main run CI but don't publish. Pushing python/vX.Y.Z (or python/vX.Y.ZbN) to the monorepo triggers the release workflow, which builds the wheel + sdist, mirrors the tag as vX.Y.Z to this repo, attaches artefacts to the GitHub Release here, and — once the package is public — publishes to PyPI.
License
Apache 2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file routeweiler-0.1.1.tar.gz.
File metadata
- Download URL: routeweiler-0.1.1.tar.gz
- Upload date:
- Size: 196.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6507dbec86983fbb053786c9f4cca698066a5cb4b66f91dd90ce634c182552d
|
|
| MD5 |
0e5636507bee43ef2b72a59b3b8c05b8
|
|
| BLAKE2b-256 |
02ece964379ab2a3f66a6d845851342963b02cf7a5c5e90463ae6e4615a2b328
|
Provenance
The following attestation bundles were made for routeweiler-0.1.1.tar.gz:
Publisher:
release-python.yml on nikoSchoinas/routeweiler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
routeweiler-0.1.1.tar.gz -
Subject digest:
f6507dbec86983fbb053786c9f4cca698066a5cb4b66f91dd90ce634c182552d - Sigstore transparency entry: 1562633711
- Sigstore integration time:
-
Permalink:
nikoSchoinas/routeweiler@d2fe52a70a2879e8c8a31f399bd6aba9fb0c45ba -
Branch / Tag:
refs/tags/python/v0.1.1 - Owner: https://github.com/nikoSchoinas
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@d2fe52a70a2879e8c8a31f399bd6aba9fb0c45ba -
Trigger Event:
push
-
Statement type:
File details
Details for the file routeweiler-0.1.1-py3-none-any.whl.
File metadata
- Download URL: routeweiler-0.1.1-py3-none-any.whl
- Upload date:
- Size: 121.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
71162c410bc4f176a47e51e247d94c6765b685eebf3186bd4158f9de9bd98eec
|
|
| MD5 |
99d93719099973c3fc5537aca065f81a
|
|
| BLAKE2b-256 |
22544f06fff412a3d84f477ad9dc531758e5e2126ece37875d6b06fc1412fc3f
|
Provenance
The following attestation bundles were made for routeweiler-0.1.1-py3-none-any.whl:
Publisher:
release-python.yml on nikoSchoinas/routeweiler
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
routeweiler-0.1.1-py3-none-any.whl -
Subject digest:
71162c410bc4f176a47e51e247d94c6765b685eebf3186bd4158f9de9bd98eec - Sigstore transparency entry: 1562633733
- Sigstore integration time:
-
Permalink:
nikoSchoinas/routeweiler@d2fe52a70a2879e8c8a31f399bd6aba9fb0c45ba -
Branch / Tag:
refs/tags/python/v0.1.1 - Owner: https://github.com/nikoSchoinas
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@d2fe52a70a2879e8c8a31f399bd6aba9fb0c45ba -
Trigger Event:
push
-
Statement type: