diff --git a/CLAUDE.md b/CLAUDE.md index fa271a0..cc227c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,31 @@ Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime; ven | `variable_cost_merchant.py` | Pay-per-actual-usage on **two protocols**: x402 upto (Permit2 + Settlement-Overrides) AND MPP tempo session (channel + SSE + mid-stream vouchers) | | `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_to_body`/`denial_reason_status`) | | `per_product_policy_merchant.py` | Multi-product merchant where each row carries its own compliance policy. One product hard-gates KYC + age + state; another is anonymous; a third uses `enforcement="soft"` (request KYC but don't block sale). Demonstrates `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | -| `signed_ucp_merchant.py` | Signed UCP profile (`/.well-known/ucp`) + JWKS endpoint (`/.well-known/jwks.json`). AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance — UCP §6 itself does NOT mandate signing; production UCP merchants commonly ship unsigned. Wires ephemeral-for-dev / env-JWK-for-prod signing, kid rotation, and `Cache-Control` posture. Uses `generate_ucp_signing_key`, `sign_ucp_profile`, `build_jwks_response`, `UCPSigningKey.from_jwk`, `UCPVerificationError`. | +| `signed_ucp_merchant.py` | Signed UCP profile (`/.well-known/ucp`) + JWKS endpoint (`/.well-known/jwks.json`). AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance — UCP §6 itself does NOT mandate signing; production UCP merchants commonly ship unsigned. Wires ephemeral-for-dev / env-JWK-for-prod signing, kid rotation, and `Cache-Control` posture. Uses `generate_ucp_signing_key`, `sign_ucp_profile`, `build_jwks_response`, `UCPSigningKey.from_jwk`, `UCPVerificationError`. Demonstrates the payment-handler builders (`mpp_payment_handler`, `x402_payment_handler`, `stripe_spt_payment_handler` — see "Payment-handler builders" below). | + +## Payment-handler builders + +The SDK ships protocol-rooted builders for the AgentScore-published payment handlers — vendors compose UCP `payment_handlers` blocks by spreading these helpers instead of hand-writing the verbose binding wrapper: + +```python +from agentscore_commerce.identity import ( + build_ucp_profile, + mpp_payment_handler, + x402_payment_handler, + stripe_spt_payment_handler, +) + +build_ucp_profile( + ..., + payment_handlers={ + **mpp_payment_handler(networks=[{"network": "tempo-mainnet", "chain_id": 4217, "recipient": "0x..."}]), + **x402_payment_handler(networks=[{"network": "base-8453", "recipient": "0x..."}]), + **stripe_spt_payment_handler(profile_id="profile_..."), + }, +) +``` + +Each helper returns `{ : [binding] }` so spreading composes the parent map. The handler `version`, spec URL, and schema URL are owned by the helpers (`agentscore_commerce/identity/ucp.py`) — bumping a handler spec version is a one-line change there. `mpp` + `x402` share the same `networks: [{network, recipient?, ...extras}]` config shape so consumers parse both identically. `recipient` is optional — omit when the merchant uses per-order recipients (e.g. Stripe-derived deposit addresses); the authoritative recipient still ships in the 402 body. ## Identity model diff --git a/README.md b/README.md index d3e6143..d841cbe 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]' | Submodule | What it provides | |---|---| | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_agentscore_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. | -| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` (for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists). | +| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. The per-framework adapter modules also expose `get_gate_quota_info(request)` for surfacing X-RateLimit info from gate state. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` (for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists). | | `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim that emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; 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 with 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). | | `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers like `awal x402 details`), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest; strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces; defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp,jwks.json}`, `/favicon.{png,ico}`; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | -| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients`: smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents to dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402`, a 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.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients`: smoke-verified CLIs the agent should use; vendor override supported; pure helper `compatible_clients_by_rails(rails)` returns the same map for vendors building custom 402s), `build_pricing_block` (cents to dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402`, a 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.stripe_multichain` | `create_multichain_payment_intent`, `get_deposit_address`, `simulate_crypto_deposit`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set, in-memory otherwise), `simulate_deposit_if_test_mode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. | | `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 and cause subtle type mismatches. | @@ -232,15 +232,9 @@ profile = build_ucp_profile( ], }, payment_handlers={ - "sh.agentscore.payment.tempo": [ - UCPPaymentHandlerBinding( - id="tempo", - version="2026-04-08", - spec="https://agentscore.sh/specification/payment-handlers/tempo", - schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", - config={"recipient": TEMPO_ADDR}, - ), - ], + **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")], # Optional: declare merchant gate policy as an `sh.agentscore.identity` capability diff --git a/agentscore_commerce/identity/__init__.py b/agentscore_commerce/identity/__init__.py index c5ef46a..81ce082 100644 --- a/agentscore_commerce/identity/__init__.py +++ b/agentscore_commerce/identity/__init__.py @@ -56,6 +56,9 @@ UCPServiceBinding, UCPSigningKey, build_ucp_profile, + mpp_payment_handler, + stripe_spt_payment_handler, + x402_payment_handler, ) from agentscore_commerce.identity.ucp_jwks import ( GeneratedUCPKey, @@ -134,11 +137,14 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "extract_x402_signer", "generate_ucp_signing_key", "is_fixable_denial", + "mpp_payment_handler", "run_gate_with_enforcement", "shipping_country_allowed", "shipping_state_allowed", "sign_ucp_profile", + "stripe_spt_payment_handler", "ucp_a2a_extension", "verification_agent_instructions", "verify_ucp_profile", + "x402_payment_handler", ] diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 74b0819..0090f97 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -395,15 +395,9 @@ async def ucp_profile(): }, signing_keys=[UCPSigningKey.from_jwk(public_jwk)], payment_handlers={ - "sh.agentscore.payment.tempo": [ - UCPPaymentHandlerBinding( - id="tempo", - version="2026-04-08", - spec="https://agentscore.sh/specification/payment-handlers/tempo", - schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", - config={"recipient": TEMPO_ADDR}, - ), - ], + **mpp_payment_handler(networks=[ + {"network": "tempo-mainnet", "chain_id": 4217, "recipient": TEMPO_ADDR}, + ]), }, name="Example Merchant", agentscore_gate=AgentScoreGatePolicy( @@ -456,6 +450,100 @@ async def ucp_profile(): ) +# ─── Payment handler builders ───────────────────────────────────────────── +# Vendors compose UCP `payment_handlers` blocks by spreading these helpers. +# The helpers fill in id/version/spec/schema/config wrapper so vendors only +# supply merchant-specific data (networks + recipients + profile_id). +# +# payment_handlers={ +# **mpp_payment_handler(networks=[...]), +# **x402_payment_handler(networks=[...]), +# **stripe_spt_payment_handler(profile_id="..."), +# } +# +# The reverse-DNS keys + spec/schema URLs + handler `version` are owned by +# these constants; bumping a handler spec version is a one-line change here. + +_HANDLER_VERSION = "2026-04-08" +_SPEC_BASE = "https://agentscore.sh/specification/payment-handlers" +_SCHEMA_BASE = "https://agentscore.sh/schemas/payment-handlers" + + +def mpp_payment_handler(*, networks: list[dict[str, Any]]) -> dict[str, list[UCPPaymentHandlerBinding]]: + """Build the `sh.agentscore.payment.mpp` payment handler block for a UCP profile. + + Each network entry: `{"network": , "chain_id"?: , "recipient"?: , ...}`. + Tempo: `tempo-mainnet` / `tempo-testnet`. Solana via `solana/charge`: + `mpp-solana-mainnet` / `mpp-solana-devnet`. + + Spread into payment_handlers: + payment_handlers={ + **mpp_payment_handler(networks=[ + {"network": "tempo-mainnet", "chain_id": 4217}, + ]), + } + """ + return { + "sh.agentscore.payment.mpp": [ + UCPPaymentHandlerBinding( + id="mpp", + version=_HANDLER_VERSION, + spec=f"{_SPEC_BASE}/mpp", + schema=f"{_SCHEMA_BASE}/mpp.json", + config={"networks": networks}, + ) + ] + } + + +def x402_payment_handler(*, networks: list[dict[str, Any]]) -> dict[str, list[UCPPaymentHandlerBinding]]: + """Build the `sh.agentscore.payment.x402` payment handler block for a UCP profile. + + Each network entry: `{"network": , "recipient"?: , ...}`. + EVM: `base-8453`, `base-84532`. Solana: `solana-mainnet-beta`, `solana-devnet`. + Stellar: `stellar-pubnet`, `stellar-testnet`. + + Spread into payment_handlers: + payment_handlers={ + **x402_payment_handler(networks=[ + {"network": "base-8453", "recipient": "0xabc..."}, + ]), + } + """ + return { + "sh.agentscore.payment.x402": [ + UCPPaymentHandlerBinding( + id="x402", + version=_HANDLER_VERSION, + spec=f"{_SPEC_BASE}/x402", + schema=f"{_SCHEMA_BASE}/x402.json", + config={"networks": networks}, + ) + ] + } + + +def stripe_spt_payment_handler(*, profile_id: str) -> dict[str, list[UCPPaymentHandlerBinding]]: + """Build the `sh.agentscore.payment.stripe_spt` payment handler block for a UCP profile. + + Spread into payment_handlers: + payment_handlers={ + **stripe_spt_payment_handler(profile_id="profile_5xKvNqM9BaH"), + } + """ + return { + "sh.agentscore.payment.stripe_spt": [ + UCPPaymentHandlerBinding( + id="stripe-spt", + version=_HANDLER_VERSION, + spec=f"{_SPEC_BASE}/stripe_spt", + schema=f"{_SCHEMA_BASE}/stripe_spt.json", + config={"rail": "stripe-spt", "profile_id": profile_id}, + ) + ] + } + + __all__ = [ "AGENTSCORE_UCP_CAPABILITY", "AgentScoreGatePolicy", @@ -466,4 +554,7 @@ async def ucp_profile(): "UCPServiceBinding", "UCPSigningKey", "build_ucp_profile", + "mpp_payment_handler", + "stripe_spt_payment_handler", + "x402_payment_handler", ] diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py index b16b8aa..e0d9fc8 100644 --- a/examples/signed_ucp_merchant.py +++ b/examples/signed_ucp_merchant.py @@ -39,13 +39,13 @@ from agentscore_commerce.identity import ( AgentScoreGatePolicy, - UCPPaymentHandlerBinding, UCPServiceBinding, UCPSigningKey, UCPVerificationError, build_jwks_response, build_ucp_profile, generate_ucp_signing_key, + mpp_payment_handler, sign_ucp_profile, verify_ucp_profile, ) @@ -122,15 +122,11 @@ async def well_known_ucp() -> JSONResponse: ], }, payment_handlers={ - "sh.agentscore.payment.tempo": [ - UCPPaymentHandlerBinding( - id="tempo", - version="2026-04-08", - spec="https://agentscore.sh/specification/payment-handlers/tempo", - schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", - config={"recipient": "0xfeedface"}, - ), - ], + **mpp_payment_handler( + networks=[ + {"network": "tempo-mainnet", "chain_id": 4217, "recipient": "0xfeedface"}, + ] + ), }, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], # Optional: declare merchant gate policy as an `sh.agentscore.identity` capability diff --git a/pyproject.toml b/pyproject.toml index 202f325..b76e9d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "1.6.0" +version = "1.7.0" description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce." readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index 0493cc2..d4744f5 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ [[package]] name = "agentscore-commerce" -version = "1.6.0" +version = "1.7.0" source = { editable = "." } dependencies = [ { name = "agentscore-py" },