Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export MPP_SECRET_KEY="$(uv run python -c 'import secrets; print(secrets.token_h
uv run --locked python -m examples.mpp_seller
```

The Seller's request callback selects its own price on every call: **0.01 USDC**
for `/api/widgets` and **0.05 USDC** for `/api/widgets?bundle=large`. The same
request-building helpers accept fixed dictionaries. For Stripe and CARD, the
callback selects **USD 1.25** or **USD 2.50**; the helpers convert dollars to cents
and attach the configured payment details.

The server listens at `http://127.0.0.1:3000`. Check both routes without paying:

```sh
Expand Down Expand Up @@ -138,7 +144,12 @@ curl -i http://127.0.0.1:3001/api/widgets
```

`/free` returns HTTP 200. `/api/widgets` returns HTTP 402 with a `PAYMENT-REQUIRED`
header describing the available offers.
header describing the available offers. Add `?bundle=large` to request **0.05 USDC**
instead. The Seller selects the price from its own price list on each request.
The callback can also return an amount and currency, such as
`{"amount": "0.05", "currency": "USDT"}`. Use `seller.resource_server(facilitator)`
with request-time `seller.offers(...)` or `seller.route(...)` prices; it registers
the InFlow schemes and keeps only the options matching the resolved price.

Run the [Buyer](x402_buyer.py) in a second terminal:

Expand Down
17 changes: 13 additions & 4 deletions examples/mpp_seller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from starlette.responses import JSONResponse

from inflowpay import ClientOptions
from inflowpay.mpp import WireObject
from inflowpay.mpp.seller import Seller


Expand All @@ -20,13 +21,20 @@ async def application(
) -> AsyncIterator[FastAPI]:
# Keep the Seller open for the whole server lifetime, not just startup.
async with await Seller.create(options, method=method) as seller:

def price(request: Request) -> WireObject:
large = request.query_params.get("bundle") == "large"
if method in ("stripe", "card"):
return {"amount": "2.50" if large else "1.25"}
return {"amount": "0.05" if large else "0.01", "currency": "USDC"}

# Stripe and CARD prices are dollars here; their challenges contain integer cents.
if method == "card":
terms = seller.card_request({"amount": "1.25"})
terms = seller.card_request(price)
elif method == "stripe":
terms = seller.stripe_request({"amount": "1.25"})
terms = seller.stripe_request(price)
else:
terms = seller.charge_request({"amount": "0.01", "currency": "USDC"})
terms = seller.charge_request(price)
app = FastAPI()

@app.get("/api/widgets")
Expand Down Expand Up @@ -67,7 +75,8 @@ async def run() -> None:
async with application(options, secret, method=method) as app:
price = "0.01 USDC" if method == "inflow" else f"1.25 USD via {method}"
print(
f"MPP: http://127.0.0.1:3000/api/widgets costs {price}; /free requires no payment.",
f"MPP: http://127.0.0.1:3000/api/widgets starts at {price}; "
"?bundle=large selects the larger bundle; /free requires no payment.",
flush=True,
)
await uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=3000)).serve()
Expand Down
23 changes: 13 additions & 10 deletions examples/x402_seller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@

import uvicorn
from fastapi import FastAPI
from x402 import x402ResourceServer
from x402.http.middleware.fastapi import payment_middleware
from x402.http.types import RouteConfig
from x402.http.types import HTTPRequestContext, RouteConfig

from inflowpay import ClientOptions
from inflowpay.x402.facilitator import Facilitator
Expand All @@ -28,16 +27,19 @@ async def application(
await Seller.create(options) as seller,
await Facilitator.create(options) as facilitator,
):
resource = x402ResourceServer(facilitator)
for registration in await seller.scheme_registrations():
resource.register(registration["network"], registration["server"])
resource = await seller.resource_server(facilitator)

def price(context: HTTPRequestContext) -> str:
large = context.adapter.get_query_param("bundle") == "large"
if instrument:
return "$2.50" if large else "$1.25"
return "0.05 USDC" if large else "0.01 USDC"

offers = await seller.offers(
"$1.25" if instrument else "0.01 USDC",
price,
schemes=["instrument"] if instrument else ["balance", "exact"],
payment_flow=payment_flow,
)
if not offers:
raise ValueError("The Seller configuration has no matching payment offers.")
app = FastAPI()
app.middleware("http")(
payment_middleware({"GET /api/widgets": RouteConfig(accepts=offers)}, resource)
Expand Down Expand Up @@ -74,9 +76,10 @@ async def run() -> None:
instrument=scheme == "instrument",
payment_flow="upfront" if payment_flow == "upfront" else "authorization",
) as app:
price = "1.25 USD via instrument" if scheme == "instrument" else "0.01 USDC"
description = "1.25 USD via instrument" if scheme == "instrument" else "0.01 USDC"
print(
f"x402: http://127.0.0.1:3001/api/widgets costs {price}; /free requires no payment.",
f"x402: http://127.0.0.1:3001/api/widgets starts at {description}; "
"?bundle=large selects the larger bundle; /free requires no payment.",
flush=True,
)
await uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=3001)).serve()
Expand Down
47 changes: 43 additions & 4 deletions src/inflowpay/mpp/seller.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import re
from collections.abc import Callable
from copy import deepcopy
from dataclasses import dataclass, field
from types import TracebackType
from typing import Self, cast
from typing import Self, TypeVar, cast, overload
from uuid import uuid4

from mpp import Credential, Receipt
Expand All @@ -30,6 +31,8 @@

__all__ = ["MppCredentialProblemError", "MppSellerConfigurationError", "Seller"]

_Request = TypeVar("_Request")


class MppSellerConfigurationError(ValueError):
pass
Expand Down Expand Up @@ -218,11 +221,23 @@ async def __aexit__(
async def aclose(self) -> None:
await self._client.aclose()

def charge_request(self, request: WireObject) -> WireObject:
@overload
def charge_request(self, request: WireObject) -> WireObject: ...

@overload
def charge_request(
self, request: Callable[[_Request], WireObject]
) -> Callable[[_Request], WireObject]: ...

def charge_request(
self, request: WireObject | Callable[[_Request], WireObject]
) -> WireObject | Callable[[_Request], WireObject]:
if self.method in ("stripe", "card"):
raise MppSellerConfigurationError(
f"Use {self.method}_request with a decimal USD amount"
)
if callable(request):
return lambda context: self.charge_request(request(context))
result = deepcopy(request)
# Use standalone pympp.pay: its high-level routes convert all prices to token units.
if "decimals" in result:
Expand All @@ -241,9 +256,21 @@ def charge_request(self, request: WireObject) -> WireObject:
}
return validate_request(self.method, "charge", result)

def stripe_request(self, request: WireObject) -> WireObject:
@overload
def stripe_request(self, request: WireObject) -> WireObject: ...

@overload
def stripe_request(
self, request: Callable[[_Request], WireObject]
) -> Callable[[_Request], WireObject]: ...

def stripe_request(
self, request: WireObject | Callable[[_Request], WireObject]
) -> WireObject | Callable[[_Request], WireObject]:
if self.method != "stripe":
raise MppSellerConfigurationError("stripe_request requires a Stripe Seller")
if callable(request):
return lambda context: self.stripe_request(request(context))
cents = _dollar_cents(request.get("amount"), "Stripe")
details = _stripe_profile(self._config)
if "metadata" in request:
Expand Down Expand Up @@ -283,9 +310,21 @@ def stripe_request(self, request: WireObject) -> WireObject:
result[key] = value
return result

def card_request(self, request: WireObject) -> WireObject:
@overload
def card_request(self, request: WireObject) -> WireObject: ...

@overload
def card_request(
self, request: Callable[[_Request], WireObject]
) -> Callable[[_Request], WireObject]: ...

def card_request(
self, request: WireObject | Callable[[_Request], WireObject]
) -> WireObject | Callable[[_Request], WireObject]:
if self.method != "card":
raise MppSellerConfigurationError("card_request requires a CARD Seller")
if callable(request):
return lambda context: self.card_request(request(context))
result = _card_profile(self._config)
result["amount"] = _dollar_cents(request.get("amount"), "CARD")
details = object_value(result["methodDetails"])
Expand Down
Loading
Loading