Skip to content

client: PaymentTransport does not advertise configured payment capabilities #222

Description

@teyrebaz33

Describe the bug

PaymentTransport.handle_async_request forwards the caller's initial request directly to the inner transport, unmodified:

response = await self._inner.handle_async_request(request)

No Accept-Payment header is added, even though PaymentRuntime already tracks every configured (method, intent) pair internally (self._methods: dict[tuple[str, str], Method], built in __init__). A server therefore has no way to know, before it issues a 402 challenge, which payment methods/intents this client actually supports.

Why this isn't hypothetical

Originally surfaced by the mpp-tools Agricola cross-SDK audit (finding AGR-2026-021), comparing this SDK's behavior against the canonical TypeScript reference implementation (wevm/mppx), which derives an Accept-Payment value from configured capabilities and adds it to initial requests by default. go, java, ruby, and rust were all flagged clean in the same audit; only python diverges.

I independently confirmed this by reading src/mpp/client/transport.py directly on current main: there is no code path, before or after the request read, that inspects self._runtime to build a header.

Steps to reproduce

from mpp.client import PaymentTransport
import httpx

transport = PaymentTransport(methods=[tempo(...)], inner=httpx.AsyncHTTPTransport())
async with httpx.AsyncClient(transport=transport) as client:
    # Inspect the outgoing request headers (e.g. via a proxy or mock transport):
    # no Accept-Payment header is present.
    await client.get("https://api.example.com/resource")

Fix

Opening a PR that adds a small PaymentRuntime.accept_payment_header() helper deriving a value like tempo/charge, stripe/charge from the already-tracked (method, intent) pairs, and has PaymentTransport set it on the initial request when the caller hasn't already supplied one explicitly. Includes regression tests for: header added when methods are configured, an explicit caller header is preserved, and no header is added when no methods are configured.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions