Skip to main content

pympp

Python SDK for the Machine Payments Protocol

PyPI License

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, pay
from mpp.methods.tempo import server as tempo

server = Mpp(methods=tempo.charge(recipient="0x742d35Cc6634c0532925a3b844bC9e7595F8fE00"))


@app.get("/paid")
@pay(server.charge(amount="0.50"))
async def handler(request, credential: Credential, receipt: Receipt):
    return JSONResponse({"data": "...", "payer": receipt.payer})

Without a framework decorator, call the route handler with the framework-neutral Request. It returns PaymentRequired (a 402 response to send as-is) or PaymentVerified:

from mpp.server import Request, Response


async def handle(request: Request) -> Response:
    result = await server.charge(amount="0.50")(request)
    if result.status != 200:
        return result.challenge
    if result.response is not None:
        return result.response  # the method handled it, such as a session top-up or close
    return result.with_receipt(Response.json({"data": "..."}))

A tempo.charge(fee_payer=...) server sponsors its payers' fees on every route, as mppx does; pass fee_payer=False to a route to opt out. As in mppx, a sponsored challenge must expire at least 15 seconds after it is issued, so a shorter-lived route on such a server raises unless it passes fee_payer=False (zero-amount and push-only routes are exempt). Before broadcasting a sponsored charge it simulates it with eth_call, which every public Tempo RPC serves.

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(methods=tempo.charge(recipient="0x..."), requires_auth=True)

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
from mpp.methods.evm import server as evm
from mpp.server import Mpp

server = Mpp(
    methods=evm.charge(
        currency=BASE_USDC,
        recipient="0x742d35Cc6634c0532925a3b844bC9e7595F8fE00",
        facilitator="https://facilitator.example.com",
    ),
)


@app.get("/paid")
@pay(server.charge(amount="0.01"))
async def handler(request, credential, receipt):
    return JSONResponse({"data": "..."})

To settle on Base mainnet through Coinbase's facilitator, install the cdp extra and pass mpp.x402.cdp.facilitator(). It reads CDP_API_KEY_ID and CDP_API_KEY_SECRET and signs a JWT for each request, as @coinbase/x402 does. Other facilitators that need per-endpoint auth take HttpFacilitator(url, create_auth_headers=...).

from mpp.x402 import cdp

method = evm.charge(currency=BASE_USDC, recipient="0x...", facilitator=cdp.facilitator())

Routes bind x402 payments the way mppx does. Routes are scoped to their HTTP method, path and query, 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.charge(); they're then bound by the echoed terms and resource URL.

Return a response object so pympp can add PAYMENT-RESPONSE. Pass methods=[tempo.charge(...), evm.charge(...)] to offer both on one route. The 402 response of a route handler already carries PAYMENT-REQUIRED. Send paid responses with Cache-Control: private so shared caches can't serve them to callers who didn't pay. with_receipt() adds it, and PAYMENT-RESPONSE, when the handler returns a response object rather than plain data.

Client

from mpp.client import Mpp
from mpp.methods.tempo import TempoAccount
from mpp.methods.tempo import client as tempo

account = TempoAccount.from_key("0x...")

async with Mpp(methods=[tempo.charge(account=account)]) as mpp:
    response = await mpp.fetch("https://mpp.dev/api/ping/paid")

To use your own httpx.AsyncClient, pass it transport=mpp.payment_transport, or call mpp.client().

A 402 with no challenge a configured method can pay raises NoMethodFoundError (a PaymentError), as mppx does; a 402 answering a paid retry is returned. To choose among payable challenges, pass order_challenges: it gets the ChallengeCandidates (challenge, method, index) in default order and returns them in preference order, and the first is paid.

mpp = Mpp(
    methods=[tempo.charge(account=account)],
    order_challenges=lambda candidates: [
        c for c in candidates if c.challenge.request.get("currency") == USDC
    ],
)

Pay x402 servers

The evm client method also pays x402 v2 exact servers (EIP-3009 USDC), such as ones built with Coinbase's x402 SDK, as mppx's client does. On a 402 carrying PAYMENT-REQUIRED, the client picks an offer in its currency and chain within max_amount, signs it and retries with PAYMENT-SIGNATURE. When a server offers both an MPP challenge the client can pay and x402, it pays MPP.

from mpp.client import Mpp
from mpp.methods.evm import BASE_USDC
from mpp.methods.evm import client as evm
from mpp.x402 import payment_response

method = evm.charge(currency=BASE_USDC, account=account, max_amount="0.01")
async with Mpp(methods=[method]) as mpp:
    response = await mpp.fetch("https://api.example.com/x402/paid")
    receipt = payment_response(response)  # PAYMENT-RESPONSE: transaction, payer, network

Permit2 offers are not 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 charges verify Shared Payment Tokens:

from mpp.methods.stripe import server as stripe

server = Mpp(methods=stripe.charge(secret_key="sk_...", network_id="bn_...", currency="usd"))

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"}}


handler = server.charge(amount="0.50", payment_intent_options=options)
# Also supported in compose offer mappings.

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 define a mpp.methods.Method (wire name, intent and pydantic request and payload models) and wrap it in a mpp.server.ServerMethod (verify, or validate and broadcast hooks) or a mpp.client.ClientMethod (create_credential). A request model may consume private route input, such as payment_intent_options, by leaving it out of the wire request.

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
x402-client pympp client paying an official x402 server
session-metered-api Sub-cent metered API and an agent paying 500 calls through one session (Docker Compose)

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 Mpp(
    methods=[tempo.charge(account=account)],
    allowed_origins=["https://api.example.com"],
) as mpp:
    response = await mpp.fetch("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.

Hosted fee-payer services

tempo.charge(fee_payer=...) also takes a hosted fee payer: its URL, or {"url": ..., "headers": ...} (or a RemoteFeePayer), where headers may be a sync or async callable returning them per request. Hosted sponsorship is on by default. Local account sponsorship requires route-level fee_payer=True; otherwise access-key clients retain an unsponsored payment path. The server enforces its sponsor policy and simulates the calls before asking the service to co-sign with eth_fillTransaction, keeps only the returned feePayerSignature and feeToken (allowed_fee_tokens restricts it), simulates the final transaction with the recovered sponsor, and broadcasts it itself. Challenges never name the service; a feePayerUrl in an old challenge is ignored.

server = Mpp(
    methods=tempo.charge(
        recipient="0x...",
        fee_payer={
            "url": "https://sponsor.example/rpc",
            "headers": {"Authorization": "Bearer ..."},
        },
    )
)

Treat the returned signature as a spending capability: callers can broadcast it without passing through pympp. Before signing, 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. pympp's checks cannot protect a sponsor that also accepts direct signing requests.

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

Route handlers bind meta into their signed challenges:

handler = server.charge(amount="1.00", meta={"account": authenticated_account_id})

Derive authenticated_account_id from trusted session authentication, not an unverified request field. Route handlers also bind the HTTP method, path and query, and the route template that framework decorators report. 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) 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.

Local fee sponsorship verifies secp256k1, P256 and WebAuthn root signatures before co-signing. Keychain sponsorship is refused locally: root-account eth_call does not enforce access-key authorization or spending limits, so even a cryptographically valid access-key signature is insufficient. Keychain and unsupported signature formats require an independently authorizing hosted sponsor that enforces those permissions before spending its gas. For locally configured charge and session routes, pass fee_payer=False to offer unsponsored payments. Local charge sponsorship is opt-in (fee_payer=True on the route); configuring a local sponsor alone does not make every charge route inaccessible to access keys.

Push payments require more than 15 seconds of challenge lifetime before signing and again immediately before submission. Their signed inclusion deadline leaves 15 seconds for returning the hash credential. This margin reduces expiry races; applications must still allow enough time for network delays and recovery.

Local sponsored charges atomically bind each payer and challenge to its first submitted transaction and allow only one pending sponsored charge per payer on a chain, across currencies. Busy payers wait up to five seconds (bounded by the new transaction's expiry). A preflight rejection releases its claims without consuming sponsorship. Submitted transactions keep their challenge binding for its acceptance window.

Unknown payer claims remain until a receipt or chain-time non-inclusion check resolves them. Wall-clock expiry alone does not release a claim; a later request for that payer performs reconciliation. Provision storage for retained records and apply capacity limits rather than evicting pending payments.

Use an atomic SponsorBudget(store=...) backend or a replay store supporting compare_and_set. The Redis adapter requires permission to execute its CAS Lua script (EVAL). Admission probes the operation before creating payment claims or sending transactions; an adapter whose CAS operation is disabled is rejected. Failed conditional cleanup retains the claim rather than deleting another request's reservation. Without a configured store these claims are process-local, so use a shared atomic store across server instances.

Session top-up sponsorship limits

Sponsored session top-ups use a rolling 24-hour allowance: five attempts per payer across its channels, 100 attempts across all payers, and an aggregate fee allowance of 100 default maximum envelope fees. After successful simulation, each submission reserves its full configured maximum fee for the window, even after success, a mined revert, or an uncertain result. Retries of the same sender-signed transaction reuse the reservation, including concurrent retries on different instances. Invalid signatures and failed simulations consume no allowance. Sponsored top-ups must be signed by the channel payer. This is conservative: unused fee allowance is not refunded. Unsponsored top-ups and server settlement/close transactions are unaffected.

from mpp.methods.tempo.session.server import SessionSponsorshipLimit

method = tempo.session(
    account=operator,
    fee_payer=sponsor,
    store=shared_store,
    top_up_sponsorship_limit=SessionSponsorshipLimit(per_payer=10, max_transactions=200),
)

Use the same shared compare-and-set store and policy on every instance using a sponsor; the default in-memory store protects only one process. The policy is operator-owned: route inputs cannot increase its limits. Reaching a limit rejects the top-up before broadcast. Raising the count does not raise the separate fee allowance; configure max_total_fee explicitly when needed. The aggregate cap is a sponsor-wide availability limit: once exhausted, new sponsored top-ups are rejected for every payer until capacity returns. Payers can submit their own fee-paying transactions through an unsponsored route. A signed 0x78 sponsorship envelope cannot be automatically converted to a payer-funded transaction without a new payer signature.

Metadata

Release files for pympp 0.20.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pympp 0.20.0
File Size Uploaded Provenance Actions
pympp-0.20.0.tar.gz 851.9 kB Without provenance Details for pympp-0.20.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pympp 0.20.0
File Interpreter ABI Platform Actions
pympp-0.20.0-py3-none-any.whl Python 3 none any Details for pympp-0.20.0-py3-none-any.whl

Total release size: 1.2 MB

Release files / pympp-0.20.0.tar.gz

Download URL pympp-0.20.0.tar.gz
Size 851.9 kB
Tags Source
SHA-256 checksum
How to use checksums
42949a1f3ca6e1374af6d6421d931172ffc2a1584672addb7f1a318a8d2c941b
BLAKE2b-256 checksum
How to use checksums
1f740b49c3cbb423932fb322ea07264b4d0a04e0d3d090d58fdd71e0bcd23660
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.16

Release files / pympp-0.20.0-py3-none-any.whl

Download URL pympp-0.20.0-py3-none-any.whl
Size 390.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6520dfd3a898889715e83717596fde52866669721c069699ffac3ec827c60686
BLAKE2b-256 checksum
How to use checksums
0f50a63ee697838f1756f857274c212d204343547e9e4a9e98ee9e1655713eff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.16

Security for pympp 0.20.0

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.

Release history Release notifications | RSS feed

This release

0.20.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page