pympp
Python SDK for the Machine Payments Protocol
Documentation
Full documentation, API reference, and guides are available at mpp.dev/sdk/python.
Install
pip install pympp
Quick Start
Server
from mpp import Credential, Receipt
from mpp.server import Mpp
from mpp.methods.tempo import tempo, ChargeIntent
server = Mpp.create(
method=tempo(
intents={"charge": ChargeIntent()},
recipient="0x742d35Cc6634c0532925a3b844bC9e7595F8fE00",
),
)
@app.get("/paid")
@server.pay(amount="0.50")
async def handler(request, credential: Credential, receipt: Receipt):
return {"data": "...", "payer": receipt.payer}
If the endpoint already uses Authorization (API keys, Bearer tokens), create the server with requires_auth=True. Challenges then advertise header="Payment-Authorization", and clients send the Payment credential in that header instead of Authorization.
server = Mpp.create(method=tempo(...), requires_auth=True)
result = await server.charge(
authorization=request.headers.get("Authorization"),
amount="0.50",
payment_authorization=request.headers.get("Payment-Authorization"),
)
Serve x402 clients
The evm method charges with EIP-3009 authorizations (USDC on Base and other
EVM chains). It serves native MPP clients and x402 v2 exact clients from the
same endpoint: unpaid responses carry PAYMENT-REQUIRED next to
WWW-Authenticate, x402 clients pay with PAYMENT-SIGNATURE, and paid x402
responses carry PAYMENT-RESPONSE. An x402 facilitator settles payments.
pip install "pympp[evm]"
from mpp.methods.evm import BASE_USDC, evm
from mpp.server import Mpp
server = Mpp.create(
method=evm(
currency=BASE_USDC,
recipient="0x742d35Cc6634c0532925a3b844bC9e7595F8fE00",
facilitator="https://facilitator.example.com",
),
)
@app.get("/paid")
@server.pay(amount="0.01")
async def handler(request, credential, receipt):
return JSONResponse({"data": "..."})
Routes bind x402 payments the way mppx does. Routes wrapped by server.pay or
compose are scoped, and with the default route_binding="required" they only
accept clients that sign mppx's extensions.mppx route binding. To accept
standard x402 clients there, such as Coinbase's SDK, pass
route_binding="resource" to evm(); they're then bound by the echoed terms and
resource URL. Unscoped server.charge() routes accept both either way.
Return a response object so pympp can add PAYMENT-RESPONSE. Pass
methods=[tempo(...), evm(...)] to offer both on one route. Servers that build
responses themselves pass payment_signature= and resource_url= to
server.charge() and add server.x402_payment_required(result, url) to 402s.
Send paid x402 responses with Cache-Control: private so shared caches can't
serve them to callers who didn't pay. server.pay adds it, and
PAYMENT-RESPONSE, when the handler returns a response object rather than plain data.
Client
from mpp.client import Client
from mpp.methods.tempo import tempo, TempoAccount, ChargeIntent
account = TempoAccount.from_key("0x...")
async with Client(methods=[tempo(account=account, intents={"charge": ChargeIntent()})]) as client:
response = await client.get("https://mpp.dev/api/ping/paid")
Custom transports can reuse method matching and credential creation without depending on HTTPX:
from mpp.client import PaymentTransport
from mpp.runtime import PaymentRuntime
payments = PaymentRuntime([method])
challenge, method = payments.match_challenge(challenges)
credential = await payments.create_credential(challenge, method)
transport = PaymentTransport(runtime=payments)
Examples
Stripe charge and composed-offer inputs accept payment_intent_options as a
mapping or a sync/async callable receiving PaymentIntentContext. The supported
fields are customer, receipt_email, metadata, and
hooks.inputs.tax.calculation. The input stays out of the signed challenge.
Resolvers run in the terminal payment operation, after available credential
validation; validate_credential() never invokes them.
async def options(context):
return {"metadata": {"order_id": "order_123"}}
result = await server.charge(
authorization,
"0.50",
payment_intent_options=options,
)
# Also supported in compose offer mappings and server.pay(...).
Both Stripe rails include SDK/challenge analytics. Existing configured metadata
retains its precedence, including the forced machine_payment="true" flag;
the new options' metadata overrides all generated/configured metadata. Crypto
recording retries a definitive optional-field rejection once without optional
fields, and remains best-effort after settlement.
Custom methods may implement the optional
prepare_intent(intent, input) -> (intent, request_input) hook to consume private
method-specific input before request construction. Return an isolated intent
view; never mutate a shared intent or execute deferred work during preparation.
Unconsumed unknown offer options are still rejected. Existing methods need no
changes, and the intent verification signatures and event payloads are unchanged.
| Example | Description |
|---|---|
| api-server | Payment-gated API server |
| fetch | CLI tool for fetching URLs with automatic payment handling |
| mcp-server | MCP server with payment-protected tools |
Protocol
Built on the "Payment" HTTP Authentication Scheme. See mpp-specs for the full specification.
License
MIT OR Apache-2.0
Restrict automatic payment origins
When fetching URLs from untrusted sources, configure exact origins before enabling payments. Origins include the scheme and port; paths and credentials are rejected. An empty allowlist disables automatic payments. Omitting it preserves the default of allowing payments to any directly requested origin.
async with Client(
methods=[tempo(account=account, intents={"charge": ChargeIntent()})],
allowed_origins=["https://api.example.com"],
) as client:
response = await client.get("https://api.example.com/paid")
Use method-level amount, currency, and recipient limits as well. A challenge realm is a protection-space name, not an authenticated origin. This policy prevents payments to unapproved origins; it cannot identify a relayed challenge served by an origin you have already approved.
External fee-payer services
An external feePayerUrl signs sponsored transactions through eth_signRawTransaction.
Treat the returned transaction as a spending capability: callers can broadcast it
without passing through pympp. Before returning a signature, the service must
validate the complete transaction and caller authorization, simulate execution,
and enforce gas-price, gas-limit, per-caller, and aggregate spending budgets.
Simulation is a point-in-time check; state can change before inclusion, so it does
not replace hard spending limits.
These requirements apply to any external sponsor. pympp validates the expected payment before requesting sponsorship, but its checks cannot protect a sponsor that also accepts direct signing requests. Local sponsorship uses pympp's local simulation path; externally signed transactions rely on the external service's pre-sign controls. Adding a simulation after receiving the signature would not prevent a caller from broadcasting that same transaction elsewhere.
Sponsored recovery records
Sponsored charge recovery records now retain the original signed transaction. Legacy records containing only a fee-token address can recover confirmed receipts while the original sponsor key is available. A missing receipt for a reconstructed transaction is not proof that the original payment failed, so these records remain non-payable until reconciled.
Before rotating sponsor keys, drain outstanding sponsored submissions. To
migrate a remaining legacy record, pause payment traffic and replace its value
under the existing mpp:charge-broadcast:<client-envelope-hash> key with the
original canonical signed 0x76 transaction from your submission log. Keep the
key unchanged and use the actual submitted bytes, not a transaction signed with
a replacement key. Resume traffic after the migration; retries will reconcile
the stored transaction. If the original bytes or key are unavailable, retain the
record for operator reconciliation rather than deleting an unknown payment.
Recovery records created by MemoryStore, SQLiteStore, and RedisStore expire
after both the authenticated challenge's expiry and the signed transaction's
valid_before plus the inclusion grace period. Retries do not extend this deadline, and a store-wide TTL does not shorten it. Replay reservations retain
their existing retention policy. Memory and SQLite reclaim abandoned expired
records during subsequent store activity; Redis expires them automatically.
Redis-backed sponsored recovery requires Redis 6.2 or later for atomic
SET NX PXAT absolute expiry.
Custom stores can implement mpp.store.ExpiringStore.put_if_absent_until(key, value, expires_at) with an atomic insert and absolute Unix timestamp to support
this cleanup. Stores without that method, and challenges without an expiry,
retain durable recovery records. Existing records keep their original retention.
Payment request context
Direct and composed payment gates bind meta into their signed challenges:
result = await payment.charge(authorization, "1.00", meta={"account": authenticated_account_id})
Derive authenticated_account_id from trusted session authentication, not an
unverified request field. Framework decorators also bind the HTTP method and
actual route, path, and query. Configured _mppx_scope is preserved in full
under the signed scope’s custom key, alongside trusted framework fields.
Custom request objects should expose their HTTP method.
Server issuance adds random opaque _mpp_nonce metadata; this key is reserved.
Nonces prevent identical challenges across callers even with fixed expirations,
but do not authenticate a presenter who stole the entire credential. Low-level
Challenge.create() remains deterministic; custom issuance must provide its own
unique authenticated context. Nonce-free challenges remain valid when their
other bindings match. HTTP credentials lacking method scope or using the old custom-scope representation
will be rejected after upgrading. Keep credentials confidential and share an
atomic replay store across workers and replicas.
This release changes HTTP method scope, custom scope representation, and direct
metadata binding as well as nonce issuance. Dict request bodies are now digested
like mppx (JSON.stringify key order) instead of with sorted keys; verifiers
still accept the old sorted-key digest, but older verifiers reject new ones. Use an atomic rollout with payment
traffic drained, or pin challenge issuance and all credential retries to the
same version until outstanding challenges expire. Merely switching all replicas
at once can still strand payments already issued by the old version.
unique_challenges=False (Mpp.create, standalone pay, or
verify_or_challenge) disables only nonce issuance; it does not make this
release compatible with old verifiers. It cannot disable the new method,
custom-scope, or metadata bindings. Mixing versions can reject already settled
hash payments and cause a client to pay again. Nonce-only staging is useful only
when all other challenge bindings already match; it temporarily retains the
fixed-expiry collision risk. Shared application authentication is still required.
Fixed Tempo split memos must not use the MPP attribution format: such a transfer can otherwise also pay the challenge named by its memo. Omit a split's memo to let the client bind it to the current challenge, or use an application memo outside the MPP format. Servers reject this configuration before issuing a challenge, and clients reject it before signing or broadcasting. Drain or replace outstanding challenges with fixed MPP memos before upgrading; their transfers cannot safely authorize the enclosing split charge.
Metadata
Release files for pympp 0.15.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pympp-0.15.1.tar.gz | 381.6 kB | Without provenance | Details for pympp-0.15.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pympp-0.15.1-py3-none-any.whl | Python 3 | none | any | Details for pympp-0.15.1-py3-none-any.whl |
Total release size: 568.3 kB
Release files / pympp-0.15.1.tar.gz
| Download URL | pympp-0.15.1.tar.gz |
|---|---|
| Size | 381.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6732807d81513e79ac1d23be0df01bd96ec03d0ff4cce2f7b91caf5009605812
|
|
BLAKE2b-256 checksum How to use checksums |
19cf82b8e50cd11ff2a155c5764d2f492c57cd81168c423e43eec7e0ab6255ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Release files / pympp-0.15.1-py3-none-any.whl
| Download URL | pympp-0.15.1-py3-none-any.whl |
|---|---|
| Size | 186.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
004aee8e2534a1be97b3da8fd9736bf67258ffcc387d50b074bc2a45d2773109
|
|
BLAKE2b-256 checksum How to use checksums |
5d625cf1266fe4713cd7d982ff0ab78127604c0869411d6669c08640c59da600
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.15
|
Security for pympp 0.15.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.