Skip to content
Merged
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
26 changes: 25 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `{ <reverse-DNS-key>: [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

Expand Down
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions agentscore_commerce/identity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
]
109 changes: 100 additions & 9 deletions agentscore_commerce/identity/ucp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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": <id>, "chain_id"?: <int>, "recipient"?: <addr>, ...}`.
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": <id>, "recipient"?: <addr>, ...}`.
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",
Expand All @@ -466,4 +554,7 @@ async def ucp_profile():
"UCPServiceBinding",
"UCPSigningKey",
"build_ucp_profile",
"mpp_payment_handler",
"stripe_spt_payment_handler",
"x402_payment_handler",
]
16 changes: 6 additions & 10 deletions examples/signed_ucp_merchant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading