Documentation Index
Fetch the complete documentation index at: /llms.txt
Use this file to discover all available pages before exploring further.
Skip to main content
Accept x402 payments through the Coinbase facilitator (mints per-endpoint CDP JWTs via
The
Returns a list of plain dicts ready for the 402 body’s
Note: ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol. Not an identity-publishing surface. ACP merchants integrate through the existing
Quick start: full merchant via the
The 402 body Checkout emits auto-attaches
Integrations
AgentScore Commerce (Python)
Identity middleware + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for agentic commerce.
agentscore-commerce is the full merchant-side SDK for agentic 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]
cdp-sdk):
pip install 'agentscore-commerce[fastapi,x402,coinbase]'
# Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in the environment.
[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,
)
accepts[]. extra.name is derived from the registered scheme metadata so the EIP-712 domain matches the on-chain USDC contract.
Submodules
| Import | What 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_result_cache: the neutral keyed JSON-value cache underneath it (same body-hash key builder); cache any probe-leg result a Checkout merchant replays on the settle leg, for example a paid upstream call made in pre_validate. 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). `malformed_payment_credential(request | headers): wire-shape gate for payment credentials (values that are not base64 JSON and not token-shaped are rejected); Checkoutruns it before any merchant hook by default, opt out withcredential_pre_check=False. 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-storewhile still spreadingdenial_reason_to_body. Returns DefaultOnDeniedResult(body, status, headers); FastAPI / Flask / aiohttp / Sanic on_deniedcallbacks 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-Tokenwith safe token hashing. Plus factories:pricing_result(cents → typedPricingResult), validation_response_(4xx envelope per framework),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.policy | Per-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 (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.payment | networks, USDC, rails registries; payment_directive, build_payment_headers (one-call WWW-Authenticate + PAYMENT-REQUIRED bundle), www_authenticate_header, payment_required_header, alias_amount_fields (opt-in v1↔v2 amount-field shim; the 402 builders do NOT apply it by default: strict x402 v2 settlement matches the whole accepts object, so an extra field breaks settle; reach for it only when a specific client is hardcoded to read maxAmountRequired), 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.30feemakessub−50−centchargesunprofitable;sub−50−centAPIspass‘includestripe=False‘explicitly),‘isevmnetwork‘/‘issolananetwork‘(CAIP−2discriminators),‘haspaymentheader‘(settle−legdiscriminator);drop−inx402helpers:‘validatex402networkconfig‘(boot−timeguard),‘verifyx402request‘(parse+validateinboundX−Payment),‘processx402settle‘(verify−then−settleinonecall),‘classifyx402settleresult‘(mapsthetaggedsettleresulttoarecommendedHTTPstatus/code/nextstepssomerchantsgetacontrolledenvelopewithoutcouplingtofacilitator−specificerrortext),‘classifyorchestrationerror‘(same‘ClassifiedX402Error‘shapebutforuncaughtexceptionsthrownelsewhereintheorchestration;conservativesubstringfallbackreturns‘None‘forunknownerrorssomerchantsrethrowinsteadofswallowing);‘zeroamountcarveout‘(skipCDP/pymppupstreamverify+settlefor0 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.discovery | is_discovery_probe_request, build_discovery_probe_response (with optional x402_sample for x402-aware crawlers; the sample envelope always carries a v2 resource and threads optional extensions, and Checkout fills both from its own url/resource_info/discovery_extensions, because envelope validators require resource and read the Bazaar example input to build valid follow-up probe bodies), 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.challenge | build_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 (pass x402=X402PaymentRequired(resource=X402ResourceInfo(...)) for x402 v2 resource metadata, url plus optional service_name / tags / description / mime_type / icon_url emitted with x402 camelCase keys, and extensions for declared extensions; both surface on the 402 body for spec-compliant crawlers, and respond_402 mirrors resource into the PAYMENT-REQUIRED header), 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_multichain | create_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.api | Everything 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,
StripeRailSpec,
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(spec=StripeRailSpec(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 a `com.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"],
),
)
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’sagentscore-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"],
),
)
Solana MPP requires a static recipient with a pre-funded token account. Point the Solana rail at a fixed wallet you control (as with
SOLANA_RECIPIENT above) and pre-fund that wallet’s USDC associated token account (ATA) once. Per-order rotating Solana deposit addresses (for example Stripe-multichain minted addresses) do not settle on Solana MPP, because the settlement transaction does not create the recipient’s token account. Tempo and x402 (Base) settle fine to per-order recipients; only Solana needs the static, pre-funded wallet. When you mint per-order recipients via agentscore_commerce.stripe_multichain, pass static_recipients={"solana": "<wallet>"} so Solana is served from your fixed wallet, or leave Solana out of the rail set.# 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)
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. Passfail_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:| Example | Scenario |
|---|---|
api_provider.py | Per-call API billing on multiple rails: Tempo MPP + x402 (Base) + Solana MPP; no compliance gate |
identity_only.py | Compliance gate without payment (vendor handles their own) |
multi_rail_merchant.py | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
stripe_multichain_merchant.py | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
variable_cost_merchant.py | Pay-per-actual-usage on two protocols: x402 upto + MPP tempo session |
compliance_merchant.py | Regulated-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.py | Multi-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.py | Signed 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. |
