Skip to main content
agentscore-commerce is the full merchant-side SDK for agent commerce in Python. One install bundles identity gating, payment-protocol helpers, 402 challenge builders, discovery doc generators, and Stripe multichain support. Submodule imports keep dependencies focused.

Installation

pip install agentscore-commerce[fastapi]   # or [flask], [django], [aiohttp], [sanic], [starlette], [stripe]
Accept x402 payments through the Coinbase facilitator (mints per-endpoint CDP JWTs via cdp-sdk):
pip install 'agentscore-commerce[fastapi,x402,coinbase]'
# Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in the environment.
The [mppx] extra adds Tempo MPP + Stripe SPT helpers.

Build the x402 accepts entry for a 402 challenge

from agentscore_commerce.payment import build_x402_accepts_for_402, create_x402_server

x402_server = await create_x402_server(facilitator="coinbase", rails=["x402-base-mainnet"])

x402_accepts = build_x402_accepts_for_402(
    x402_server,
    network="eip155:8453",
    price=f"${total_usd}",
    pay_to=os.environ["TREASURY_BASE_RECIPIENT"],
    max_timeout_seconds=300,
)
Returns a list of plain dicts ready for the 402 body’s accepts[]. extra.name is derived from the registered scheme metadata so the EIP-712 domain matches the on-chain USDC contract.

Submodules

ImportWhat it gives you
agentscore_commerce (top-level: the 2.0 high-level surface)Checkout orchestrator + CheckoutContext + CheckoutGateConfig + CheckoutValidationError + DiscoveryProbeConfig + SettleOutcome + MppxComposeOutcome + PricingResult (one config object, hooks for pre_validate/compute_pricing/on_settled/mint_recipients/compose_mppx, auto-derived x402+pympp servers, per-framework adapters handle_fastapi/handle_flask/handle_django/handle_aiohttp/handle_sanic, signed UCP routes via mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}). Plus compute_first_checkout — variable-cost pay-per-result helper (compute-first + exact-x402) that works on every exact-mode rail without upto / Permit2 / Settlement-Overrides; create_quote_cache — content-hash quote cache used by the compute-first helper (in-memory default; pass redis_url for distributed deployments). create_default_on_denied({merchant_name, support_email, support_context?, wallet_not_trusted_message?, payment_required_message?}) — canonical on_denied(reason) -> DefaultOnDeniedResult factory matching Checkout’s gate hook (handles wallet_signer_mismatch, wallet_not_trusted unfixable fallback, payment_required, token_expired/invalid_credential/api_error). has_payment_header(request_or_headers) — discriminator that splits discovery legs (no payment credential → 402) from settle legs (payment-signature / x-payment / Authorization: Payment JWT-BYTES); has_x402_header / has_mppx_header — granular dispatch helpers (x402 vs MPP credential present). default_read_only_on_denied(reason) — canonical on_denied for read-only resource gates (GET /orders/:id): collapses every denial to 401 unauthorized + Cache-Control: no-store while still spreading denial_reason_to_body. Returns DefaultOnDeniedResult(body, status, headers); FastAPI / Flask / aiohttp / Sanic on_denied callbacks accept an optional 3-tuple (body, status, headers) to thread headers through. extract_owner_scope(headers) -> OwnerScope — pull canonical owner identity from X-Wallet-Address / X-Operator-Token with safe token hashing. Plus factories: pricing_result (cents → typed PricingResult), validation_response_{fastapi,flask,django,aiohttp,sanic} (4xx envelope per framework), Receipt/ReceiptNextSteps/ProductInfo/ShippingAddress (canonical 200-receipt dataclasses, universal across goods + API merchants), make_mppx_compose_hook.
agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}Trust gate middleware: KYC, age, sanctions (account name + signer wallet), jurisdiction. AgentScoreGate(...) (or agentscore_gate(app, ...) on Flask/Sanic), capture_wallet(...), get_signer_verdict(...), get_agentscore_data(...). Each adapter also exports a conditional variant that wraps the gate so it fires only on settle legs (anonymous discovery flows through and gets a 402 with all rails): FastAPI / ASGI / Django expose ConditionalAgentScoreGate; Flask + Sanic expose conditional_agentscore_gate(app, ...); aiohttp exposes conditional_agentscore_gate_middleware(...). The existing agentscore_gate(app, ...) and AgentScoreGate(...) accept an optional condition= callable for inline gating.
agentscore_commerce.identity.policyPer-product compliance helpers: PolicyBlock, build_gate_from_policy, run_gate_with_enforcement, shipping_country_allowed, shipping_state_allowed, validate_shipping_against_policy (one-call country+state validator raising CheckoutValidationError with the canonical envelope on miss). All five are also re-exported at agentscore_commerce top-level.
agentscore_commerce.identity (publishers + key/token helpers)Cross-vendor identity-publishing helpers: build_a2a_agent_card (Google A2A v1.0 Signed Agent Card, served at /.well-known/agent-card.json), build_ucp_profile (Google Universal Commerce Protocol, served at /.well-known/ucp). Both return unsigned dataclasses with to_dict(): vendor signs + publishes. Key + token helpers: load_ucp_signing_key_from_env + LoadUCPSigningKeyOptions (cached env-driven loader: reads UCP_SIGNING_KEY_JWK_PRIVATE JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via threading.Lock); hash_operator_token (sha256 hex of plaintext opc_... — for merchants persisting operator_token_id to their own DB without ever storing the plaintext); extract_owner_scope(headers) -> OwnerScope (canonical owner-identity extractor for caller-scoped resource queries — reads X-Wallet-Address / X-Operator-Token, hashes the token so plaintext never leaves the request).
agentscore_commerce.paymentnetworks, USDC, rails registries; payment_directive, build_payment_headers (one-call WWW-Authenticate + PAYMENT-REQUIRED bundle), www_authenticate_header, payment_required_header, alias_amount_fields (v1↔v2 amount field shim for v1-only x402 parser compat), settlement_override_header, dispatch_settlement_by_network, extract_payment_signer (accepts positional x402_payment_header AND/OR authorization_header= kwarg; recovers signer from x402 EIP-3009 payload.authorization.from OR MPP Authorization: Payment BASE64 did:pkh:eip155:CHAIN:ADDR / did:pkh:solana:GENESIS:ADDR source DID), parse_did_pkh_address (parses raw did:pkh:FAMILY:CHAIN:ADDR DIDs into a typed PaymentSigner; used by extract_payment_signer and re-exported for callers receiving a typed source string from pympp), detect_rail_from_headers (returns "x402" / "mpp" / None from inbound headers), build_idempotency_key; create_x402_server, create_mppx_server; build_default_checkout_rails(tempo=, x402_base=, solana_mpp=, stripe=) (canonical 4-rail rails dict factory: merchants pass per-rail overrides instead of redeclaring the recipient sentinel + network/chain_id/token boilerplate. Flipping network alone derives the right token / chain_id: Base Sepolia → Sepolia USDC + chain_id 84532, Solana devnet → devnet USDC mint. Solana’s network accepts CAIP-2 or the raw @solana/mpp form mainnet-beta / devnet / localnet), build_mppx_compose_rails(amount_usd=, tempo_recipient=, solana_recipient=, ...) (per-call intent factory replacing the hand-rolled [("tempo/charge", {...}), ("solana/charge", {...}), ("stripe/charge", {...})] list; auto-handles USD→atomic conversion for Solana; auto-drops stripe/charge when amount_usd < 0.50 since Stripe’s fixed ~0.30feemakessub50centchargesunprofitablesub50centAPIspassincludestripe=Falseexplicitly),isevmnetwork/issolananetwork(CAIP2discriminators),haspaymentheader(settlelegdiscriminator);dropinx402helpers:validatex402networkconfig(boottimeguard),verifyx402request(parse+validateinboundXPayment),processx402settle(verifythensettleinonecall),classifyx402settleresult(mapsthetaggedsettleresulttoarecommendedHTTPstatus/code/nextstepssomerchantsgetacontrolledenvelopewithoutcouplingtofacilitatorspecificerrortext),classifyorchestrationerror(sameClassifiedX402Errorshapebutforuncaughtexceptionsthrownelsewhereintheorchestration;conservativesubstringfallbackreturnsNoneforunknownerrorssomerchantsrethrowinsteadofswallowing);zeroamountcarveout(skipCDP/pymppupstreamverify+settlefor0.30 fee makes sub-50-cent charges unprofitable — sub-50-cent APIs pass `include_stripe=False` explicitly), `is_evm_network` / `is_solana_network` (CAIP-2 discriminators), `has_payment_header` (settle-leg discriminator); drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle in one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text), `classify_orchestration_error` (same `ClassifiedX402Error` shape but for uncaught exceptions thrown elsewhere in the orchestration; conservative substring fallback returns `None` for unknown errors so merchants rethrow instead of swallowing); `zero_amount_carve_out` (skip CDP / pympp upstream verify+settle for 0 settles where the upstream rejects value=0 payloads; parses the credential, lifts signer + network, returns a ZeroSettleResult shaped identically to the success path so callers branch on rail, not on result shape); usd_to_atomic (Decimal-based USD → atomic int, ROUND_HALF_UP, rejects NaN / negative / infinite — for Tempo / Solana / Base USDC amount construction).
agentscore_commerce.discoveryis_discovery_probe_request, build_discovery_probe_response (with optional x402_sample for x402-aware crawlers), sample_x402_accept_for_network, build_well_known_mpp, build_well_known_x402 (x402scan v1 /.well-known/x402 shape), build_llms_txt, build_skill_md (Claude-Skill-compatible /skill.md agent-discovery manifest), build_redemption_skill_md (delivery-neutral redemption-code template — printed mailers, emailed codes, API trial credits all covered; endpoint_path/delivery_intro/body_shape/body_rules/extra_recovery_rows overrides for non-goods shapes), build_merchant_index_json + standard_endpoint_descriptions(kind=) (canonical / discovery body for goods vs api merchants), build_success_next_steps (universal Passport-active success block), build_agentscore_onboarding_steps, agentscore_openapi_snippets, siwx_security_scheme + x_payment_info_extension + x_guidance_extension, build_bazaar_discovery_payload; NoindexNonDiscoveryMiddleware (ASGI) + install_flask_noindex (Flask) + DjangoNoindexMiddleware (Django): emit X-Robots-Tag: noindex on every path except the agent-discovery surfaces; pure helpers is_discovery_path + DEFAULT_DISCOVERY_PATHS for any framework not listed. Plus the UCP/JWKS publish surface: build_signed_ucp_response, build_signed_jwks_response, well_known_preflight_response, default_a2a_services, bootstrap_ucp_signing_key, framework-neutral SignedDiscoveryResponse + per-framework wrappers signed_response_{fastapi,flask,django,aiohttp,sanic}.
agentscore_commerce.challengebuild_accepted_methods, build_identity_metadata (auto-attached by Checkout when wallet header present), build_how_to_pay, build_agent_instructions (auto-emits per-rail compatible_clients; pure helper compatible_clients_by_rails(rails) returns the same map for vendors building custom 402s), build_402_body, build_pricing_block (cents → dollar-string; optional shipping_cents + discount_cents — pass discount_cents for redemption codes / coupons so the 402 body surfaces discount alongside subtotal / total for agent-renderable savings; optional decimals for sub-cent precision so per-token / per-byte unit pricing advertises real amounts instead of rounding to two decimals), first_encounter_agent_memory, Receipt/ReceiptNextSteps/ProductInfo/ShippingAddress dataclasses (canonical 200-receipt shape); respond_402: drop-in 402 emit that preserves pympp’s WWW-Authenticate and layers x402’s PAYMENT-REQUIRED. build_validation_error: structured 4xx body builder ({error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}) so vendors compose body shapes by name instead of inlining at every validation site.
agentscore_commerce.middleware.{fastapi,flask,django,aiohttp,sanic,asgi}Framework-specific rate-limit middleware. FastAPI: rate_limit_fastapi(...) (FastAPI dependency) plus the ASGI RateLimitMiddleware re-export. Flask: rate_limit_flask(app, ...) installer. Django: class-based async RateLimitMiddleware configured via settings.AGENTSCORE_RATE_LIMIT. aiohttp: rate_limit_aiohttp(...) middleware factory. Sanic: rate_limit_sanic(app, ...) installer. asgi.RateLimitMiddleware works with any starlette-compatible app. Shared options: window_seconds (default 60), max_requests (default 60), key_resolver (default first hop of x-forwarded-for), redis_url (lazy-imports redis.asyncio when set, in-memory dict fallback otherwise), key_prefix. redis is an optional peer dep (install via the redis extra).
agentscore_commerce.stripe_multichaincreate_multichain_payment_intent (returns MultichainPaymentIntentResult; read result.deposit_addresses[network] directly), create_pay_to_address_from_stripe_pi(authorization_header=, amount_cents=, stripe=, pi_cache=, networks=, static_recipients=, metadata=, order_id=, preferred_network=) — per-order payTo resolver matching Checkout.mint_recipients: on the settle leg, reuses the buyer’s signed-against payTo from the MPP credential (after pi_cache.has_address check OR a static_recipients match — the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by static_recipients, caches the merged map, registers static addresses with pi_cache.cache_address so verify-leg lookups pass. mint_multichain_recipients(...same kwargs) — structured variant returning MintMultichainRecipientsResult(recipients, payment_intent_id, reused_from_credential) for the full per-rail map (typical multi-rail merchant hook). Use static_recipients={"solana": "<wallet>"} for low-margin endpoints where Solana per-call ATA rent (~$0.50 against MPP spec §13.6) dominates revenue — the SDK skips Stripe minting on that network and reuses the static recipient forever; pair with a one-time external USDC transfer to pre-create the recipient’s USDC ATA and every settle pays only the per-tx fee. SolanaMppRailSpec.ata_creation_required defaults to True (data-only — solana method registration through create_mppx_server is a follow-up; merchants building the solana method directly via pympp should pass ata_creation_required themselves to the charge factory). simulate_crypto_deposit, create_mppx_stripe; create_pi_cache (TTL’d PI / deposit-address cache, Redis-backed when redis_url set), simulate_deposit_if_test_mode (gates on sk_test_ and looks up the PI for you), simulate_deposit_for_outcome(outcome=, deposit_address=, get_payment_intent_id=, stripe_secret_key=, stripe_version=) (dispatches the simulator based on a Checkout / compute_first_checkout settle outcome), network_for_outcome (outcome → simulator network arg, handles both Checkout-shaped rail_key and compute-first-shaped mpp_method, accepts bare scheme names AND SCHEME/charge forms), STRIPE_TEST_TX_HASH_SUCCESS / STRIPE_TEST_TX_HASH_FAILED constants.
agentscore_commerce.apiEverything from agentscore-py re-exported in one place: AgentScore + AgentScoreError, AGENTSCORE_TEST_ADDRESSES + is_agentscore_test_address. Don’t add agentscore-py as a separate dep: the two can drift versions.

Identity model

Two identity types: wallet (X-Wallet-Address) and operator-token (X-Operator-Token). Default checks operator-token first, then wallet. Address normalization is network-aware: EVM lowercased, Solana base58 preserved verbatim. DenialReason codes (missing_identity, identity_verification_required, token_expired, invalid_credential, wallet_signer_mismatch, wallet_auth_requires_wallet_signing, wallet_not_trusted, api_error, payment_required) each carry a structured agent_instructions JSON block describing concrete recovery actions. create_session_on_missing auto-mints a verification session for two paths: cold-start (no identity headers) AND wallet_not_trusted with fixable reasons (kyc_required / kyc_pending / kyc_failed). Both paths rewrite the denial to identity_verification_required before reaching on_denied, so merchants only handle one code. When the merchant omits create_session_on_missing from the gate config, Checkout auto-defaults it from gate.api_key + gate.base_url + gate.context + gate.merchant_name; merchants that need on_before_session side effects (e.g. pre-minting an order_id) supply their own config to override. build_verification_required_body(reason, message=?, agent_instructions=?, extra=?) collapses the per-merchant body-mapping boilerplate. The gate middleware extracts the payment signer pre-evaluate (extract_payment_signer(x402_header)) and passes signer={address, network} to /v1/assess; one round trip now carries the gate verdict AND the wallet-signer-match outcome AND the signer_sanctions OFAC SDN wallet-address verdict. Merchants read both back synchronously via get_signer_verdict(request) off the gate’s cache (no extra HTTP call). Wallet-OFAC SDN enforcement is unconditional whenever the signer is supplied — an SDN hit OR an unavailable wallet-sanctions lookup flips the gate decision to deny before the handler runs (fail-closed; OFAC strict-liability), without requiring policy.require_sanctions_clear to opt in. policy.require_sanctions_clear is the separate NAME-based screen on the operator’s KYC identity. capture_wallet is fire-and-forget; POSTs the signer to /v1/credentials/wallets so the operator’s cross-merchant credential↔wallet profile builds up over time.

Identity publishing (cross-vendor standards)

Two helpers compose AgentScore identity into the payload formats published to other agent-commerce ecosystems. Each returns an unsigned dataclass; your service signs + serves it however its key infrastructure works.
from agentscore_commerce import (
    A2AAgentSkill,
    AgentScoreGatePolicy,
    UCPPaymentHandlerBinding,
    UCPServiceBinding,
    UCPSigningKey,
    build_a2a_agent_card,
    build_ucp_profile,
    ucp_a2a_extension,
)

# 1. Google A2A v1.0 Signed Agent Card, published at /.well-known/agent-card.json.
#    Per UCP §A2A binding (https://ucp.dev/specification/checkout-a2a/), agents that
#    serve UCP via the A2A transport MUST declare the canonical UCP extension URI in
#    capabilities.extensions[] so platforms arriving via A2A detect UCP support
#    without re-fetching the profile. Pass an empty capabilities map when you serve
#    UCP at the discovery layer but have no formal capability bindings yet.
#    Skills declare what the agent can do; they are AgentSkill objects (top-level on
#    the card). Identity claims live in a separate AgentCardSignature (RFC 7515 JWS)
#    wrapping the serialized card, NOT in the card body itself.
card = build_a2a_agent_card(
    name="My Agent Service",
    description="Buy products via agent payments.",
    url="https://agents.example.com",
    version="1.0.0",
    skills=[
        A2AAgentSkill(
            id="purchase",
            name="Purchase",
            description="Buy products via agent payments.",
            tags=["commerce", "payment"],
        ),
    ],
    extensions=[ucp_a2a_extension()],
)

# 2. Google Universal Commerce Protocol profile, published at /.well-known/ucp
#    Output shape: {"ucp": {"version", "services", "capabilities",
#    "payment_handlers", "name?", "supported_versions?"}, "signing_keys": [...]}
#    — services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
#    service / capability / handler name (UCP spec §3 + §6).
profile = build_ucp_profile(
    name="My Agent Service",
    services={
        "dev.ucp.shopping": [
            UCPServiceBinding(
                version="2026-04-08",
                spec="https://ucp.dev/2026-04-08/specification/overview",
                transport="mcp",
                endpoint="https://agents.example.com/api/ucp/mcp",
                schema="https://ucp.dev/services/shopping/mcp.openrpc.json",
            ),
        ],
    },
    payment_handlers={
        **mpp_payment_handler(networks=[{"network": "tempo-mainnet", "chain_id": 4217, "recipient": TEMPO_ADDR}]),
        **x402_payment_handler(networks=[{"network": "base-8453", "recipient": BASE_ADDR}]),
        **stripe_spt_payment_handler(profile_id="profile_5xKvNqM9BaH"),
    },
    signing_keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "...", "y": "..."})],
    # Optional: declare merchant gate policy as an `sh.agentscore.identity` capability
    # binding inside the public profile. Static policy declaration only — no per-operator
    # claims. Per-operator identity attestation flows through the AP2 risk-signal endpoint.
    agentscore_gate=AgentScoreGatePolicy(
        require_kyc=True, min_age=21, allowed_jurisdictions=["US"],
    ),
)
Note: ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol. Not an identity-publishing surface. ACP merchants integrate through the existing build_402_body + build_payment_headers + Stripe SPT rail.

Signing UCP profiles (vendor extension; opt-in for trust-mode verifiers)

UCP §6 doesn’t mandate profile-body JWS signing; production UCP merchants commonly ship unsigned. AgentScore’s agentscore-profile+jws is a vendor extension layered on top of the unsigned UCP profile for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance. Vanilla UCP agents read the canonical body and ignore the signature field. agentscore-commerce ships four helpers that ride on the joserfc optional dep (pip install agentscore-commerce[ucp]):
from agentscore_commerce import (
    UCPSigningKey,
    UCPVerificationError,
    build_jwks_response,
    generate_ucp_signing_key,
    sign_ucp_profile,
    verify_ucp_profile,
)

# One-time per merchant: generate or load a key. Persist private_key in your secrets manager.
key = generate_ucp_signing_key(kid="merchant-2026-05")

# On every /.well-known/ucp request:
profile = build_ucp_profile(
    name="My Agent Service",
    services={...},  # keyed by service name, see above
    payment_handlers={...},  # keyed by handler reverse-DNS name, see above
    signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
)
signed = sign_ucp_profile(
    profile.to_dict(),
    signing_key=key.private_key,
    kid=key.public_jwk["kid"],
    alg="EdDSA",
)
return signed

# On /.well-known/jwks.json:
return build_jwks_response([key.public_jwk])
verify_ucp_profile enforces the JWS protected header typ='agentscore-profile+jws' (vendor-namespaced — UCP §6 doesn’t define a profile-as-JWS typ), restricts alg to EdDSA/ES256, requires a kid, rejects duplicate kids, and compares canonical body bytes against the JWS payload. Failures raise UCPVerificationError with a discriminated code attribute. sign_ucp_profile rejects profiles containing float values: cross-language float canonicalization is not stable. Use decimal strings (e.g. "9.99") for monetary or fractional fields in extras. Persisting the private JWK. Mint once via generate_ucp_signing_key(), serialize via key.private_key.as_dict(private=True), store in your secret manager. On each container start, read the secret, OKPKey.import_key(jwk_dict) (or ECKey.import_key for ES256) to re-hydrate. KMS-backed flows require subclassing the joserfc Key to delegate the sign hook. Key rotation. Mint a new key with a new kid, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Drop the old JWK after your verifier-side cache TTL has elapsed. Inline JWK in the profile vs separate JWKS endpoint. UCP §6 mandates the separate /.well-known/jwks.json endpoint as the canonical trust source. The profile’s signing_keys[] is informational; verifiers MUST resolve the kid against the JWKS to prevent a swap-after-sign attack. Verifiers reconstruct the canonical body (everything except signature, keys sorted at every level), look up the kid in JWKS, and check the JWS. verify_ucp_profile(signed, jwks) does this for you. Both EdDSA (Ed25519) and ES256 are supported. EdDSA is the default and recommended. Profiles signed by Node verify in Python and vice versa: cross-language byte-identical canonicalization.

Quick start: full merchant via the Checkout orchestrator

Checkout is the canonical merchant surface in 2.0 — one config object, hooks for the merchant-specific pieces, and the SDK handles 402 emit, identity gating, x402 verify+settle, mppx compose, $0 carve-out, identity_metadata auto-attach, and the per-framework adapter. Most merchants reach for Checkout first and only drop to lower-level helpers when they need custom flows (variable-cost streaming, multi-protocol composition).
import os
from fastapi import FastAPI, Request
from agentscore_commerce import (
    Checkout, CheckoutGateConfig, DiscoveryProbeConfig, PricingResult, pricing_result,
    SolanaMppRailSpec, StripeRailSpec, TempoRailSpec, X402BaseRailSpec,
    validate_shipping_against_policy,
)
from agentscore_commerce.discovery import default_a2a_services

app = FastAPI()


async def _pre_validate(ctx):
    body = ctx.request.body or {}
    product = await lookup_product(body.get("product_slug"))
    validate_shipping_against_policy(
        country=body.get("shipping", {}).get("country", ""),
        state=body.get("shipping", {}).get("state", ""),
        policy=product,
        product_name=product["name"],
    )
    return {"product": product}


async def _compute_pricing(ctx) -> PricingResult:
    return pricing_result(
        subtotal_cents=ctx.state["product"]["price_cents"],
        tax_cents=ctx.state["product"]["tax_cents"],
        tax_rate=ctx.state["product"]["tax_rate"],
        tax_state=ctx.state["product"]["tax_state"],
    )


async def _on_settled(ctx, outcome):
    return {"ok": True, "order_id": ctx.reference_id, "tx_hash": outcome.tx_hash}


checkout = Checkout(
    rails={
        "tempo":     TempoRailSpec(recipient=os.environ["TEMPO_RECIPIENT"]),
        "x402_base": X402BaseRailSpec(recipient=os.environ["X402_BASE_RECIPIENT"], network="eip155:8453"),
        "solana_mpp":SolanaMppRailSpec(recipient=os.environ["SOLANA_RECIPIENT"], network="solana:mainnet"),
        "stripe":    StripeRailSpec(profile_id=os.environ["STRIPE_PROFILE_ID"]),
    },
    url="https://merchant.example/purchase",
    pre_validate=_pre_validate,
    compute_pricing=_compute_pricing,
    on_settled=_on_settled,
    cdp_api_key_id=os.environ.get("CDP_API_KEY_ID"),
    cdp_api_key_secret=os.environ.get("CDP_API_KEY_SECRET"),
    mppx_secret_key=os.environ.get("MPP_SECRET_KEY"),
    gate=CheckoutGateConfig(
        api_key=os.environ["AGENTSCORE_API_KEY"],
        merchant_name="Merchant",
        require_kyc=True, require_sanctions_clear=True, min_age=21, allowed_jurisdictions=["US"],
    ),
    # Optional: empty-body POSTs without a payment header auto-route to a sample 402
    # so x402 crawlers (awal x402 details, x402-proxy, ...) can discover the surface.
    discovery_probe=DiscoveryProbeConfig(
        realm="merchant.example",
        sample_rail="tempo-mainnet",
        sample_amount_usd=1.0,
        sample_recipient=os.environ["TEMPO_RECIPIENT"],
    ),
)

# One-call mount: GET /.well-known/ucp + GET /.well-known/jwks.json + OPTIONS preflights.
# Flask / Django / aiohttp / Sanic each have a sibling helper
# (`mount_ucp_routes_flask` etc.) with the same kwargs.
checkout.mount_ucp_routes_fastapi(
    app,
    name="Merchant",
    well_known_ucp_url="https://merchant.example/.well-known/ucp",
    services=default_a2a_services(agent_card_url="https://merchant.example/.well-known/agent-card.json"),
    signing_kid="merchant-2026-05",
)


@app.post("/purchase")
async def purchase(request: Request):
    return await checkout.handle_fastapi(request)
The 402 body Checkout emits auto-attaches identity_mode + required_signer + signer_constraint (and linked_wallets when the gate populated them) when an inbound X-Wallet-Address header is present — so agents self-correct at discovery instead of at the 403 retry.

Fail-open (opt-in)

By default AgentScore Gate fails closed on AgentScore-side infra failure (429 / 5xx / network timeout); buyer gets 503. Pass fail_open=True to opt in to graceful degradation, then read the per-request degraded state via get_gate_degraded_state(request):
from fastapi import Depends, FastAPI, Request
from agentscore_commerce.identity.fastapi import AgentScoreGate, get_gate_degraded_state

app = FastAPI()
gate = AgentScoreGate(api_key=os.environ["AGENTSCORE_API_KEY"], fail_open=True)

@app.post("/purchase", dependencies=[Depends(gate)])
async def purchase(request: Request):
    state = get_gate_degraded_state(request)
    if state["degraded"]:
        logger.warning("gate degraded: %s", state["infra_reason"])
    # ...
get_gate_degraded_state is exported by every Python adapter (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI middleware) and reads the framework-appropriate request state. Signatures take a request argument on every adapter except Flask, which reads from g and takes no arguments (get_gate_degraded_state()). Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of fail_open; see compliance-gating › Fail-open behavior.

Examples

The examples/ directory has runnable single-file FastAPI apps for each common merchant scenario:
ExampleScenario
api_provider.pyPer-call API billing on multiple rails: Tempo MPP + x402 (Base + Solana); no compliance gate
identity_only.pyCompliance gate without payment (vendor handles their own)
multi_rail_merchant.pyFull agent-commerce: identity + Tempo MPP + x402 + Stripe SPT
stripe_multichain_merchant.pyStripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses)
variable_cost_merchant.pyPay-per-actual-usage on two protocols: x402 upto + MPP tempo session
compliance_merchant.pyRegulated-goods merchant: full compliance gate + custom on_denied composing the denial helpers (verification_agent_instructions, is_fixable_denial, build_signer_mismatch_body, build_contact_support_next_steps, denial_reason_status / denial_reason_to_body to assemble the (status, body) pair for any DenialReason)
per_product_policy_merchant.pyMulti-product merchant where each row carries its own compliance policy (hard gate, soft, or none). Uses PolicyBlock, build_gate_from_policy, run_gate_with_enforcement, and the one-call validate_shipping_against_policy.
signed_ucp_merchant.pySigned UCP profile + JWKS endpoint via the one-call checkout.mount_ucp_routes_fastapi(app, ...). Demonstrates ephemeral-for-dev / env-JWK-for-prod and bootstrap_ucp_signing_key lifespan-hook usage.