Metadata-Version: 2.5
Name: pympp
Version: 0.15.1
Summary: Python SDK for the Machine Payments Protocol (MPP)
Project-URL: Homepage, https://github.com/tempoxyz/pympp
Project-URL: Documentation, https://github.com/tempoxyz/pympp#readme
Author: Tempo
License: MIT OR Apache-2.0
License-File: LICENSE-APACHE
License-File: LICENSE-MIT
Classifier: License :: OSI Approved :: Apache Software License
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: rfc8785>=0.1.4
Provides-Extra: dev
Requires-Dist: hypothesis>=6.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff==0.16.6; extra == 'dev'
Requires-Dist: starlette>=0.37; extra == 'dev'
Provides-Extra: evm
Requires-Dist: eth-account>=0.11; extra == 'evm'
Requires-Dist: eth-hash[pycryptodome]>=0.7; extra == 'evm'
Provides-Extra: mcp
Requires-Dist: anyio>=4.5; extra == 'mcp'
Requires-Dist: mcp<2,>=1.1; extra == 'mcp'
Requires-Dist: propcache==0.5.2; extra == 'mcp'
Requires-Dist: pyjwt==2.15.1; extra == 'mcp'
Requires-Dist: uvicorn==0.52.4; extra == 'mcp'
Requires-Dist: yarl==1.24.5; extra == 'mcp'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Provides-Extra: server
Requires-Dist: pydantic>=2.0; extra == 'server'
Provides-Extra: sqlite
Requires-Dist: aiosqlite>=0.20; extra == 'sqlite'
Provides-Extra: stripe
Requires-Dist: attrs>=23.0; extra == 'stripe'
Requires-Dist: eth-abi<6,>=5.0; extra == 'stripe'
Requires-Dist: eth-account>=0.11; extra == 'stripe'
Requires-Dist: eth-hash[pycryptodome]>=0.7; extra == 'stripe'
Requires-Dist: propcache==0.5.2; extra == 'stripe'
Requires-Dist: pydantic>=2.0; extra == 'stripe'
Requires-Dist: pyjwt==2.15.1; extra == 'stripe'
Requires-Dist: pytempo>=0.2.1; extra == 'stripe'
Requires-Dist: rlp>=4.0; extra == 'stripe'
Requires-Dist: stripe>=8.0.0; extra == 'stripe'
Requires-Dist: urllib3==2.8.0; extra == 'stripe'
Requires-Dist: yarl==1.24.5; extra == 'stripe'
Provides-Extra: tempo
Requires-Dist: attrs>=23.0; extra == 'tempo'
Requires-Dist: eth-abi<6,>=5.0; extra == 'tempo'
Requires-Dist: eth-account>=0.11; extra == 'tempo'
Requires-Dist: eth-hash[pycryptodome]>=0.7; extra == 'tempo'
Requires-Dist: propcache==0.5.2; extra == 'tempo'
Requires-Dist: pydantic>=2.0; extra == 'tempo'
Requires-Dist: pyjwt==2.15.1; extra == 'tempo'
Requires-Dist: pytempo>=0.2.1; extra == 'tempo'
Requires-Dist: rlp>=4.0; extra == 'tempo'
Requires-Dist: urllib3==2.8.0; extra == 'tempo'
Requires-Dist: yarl==1.24.5; extra == 'tempo'
Description-Content-Type: text/markdown

# pympp

Python SDK for the [**Machine Payments Protocol**](https://mpp.dev)

[![PyPI](https://img.shields.io/pypi/v/pympp.svg)](https://pypi.org/project/pympp/)
[![License](https://img.shields.io/pypi/l/pympp.svg)](LICENSE)

## Documentation

Full documentation, API reference, and guides are available at **[mpp.dev/sdk/python](https://mpp.dev/sdk/python)**.

## Install

```bash
pip install pympp
```

## Quick Start

### Server

```python
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`.

```python
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.

```bash
pip install "pympp[evm]"
```

```python
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

```python
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:

```python
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.

```python
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](./examples/api-server/) | Payment-gated API server |
| [fetch](./examples/fetch/) | CLI tool for fetching URLs with automatic payment handling |
| [mcp-server](./examples/mcp-server/) | MCP server with payment-protected tools |

## Protocol

Built on the ["Payment" HTTP Authentication Scheme](https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/). See [mpp-specs](https://tempoxyz.github.io/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.

```python
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`](https://redis.io/docs/latest/commands/set/) 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:

```python
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.
