Skip to content

Commit c0f9e8e

Browse files
vvillait88claude
andcommitted
feat(challenge): emit compatible_clients in 402 agent_instructions
Mirror the node-commerce gain — add a `compatible_clients` field to the 402 body via build_agent_instructions, derived per-rail from how_to_pay. Same default client list, same shape, same vendor-override pattern via BuildAgentInstructionsInput(compatible_clients={...}). CLAUDE.md gets the symmetric documentation section + structural alignment with node-commerce/CLAUDE.md (Examples table + peer-dep paragraph + "Every helper lifts from production code" mantra at the top, matching node-commerce). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a750b12 commit c0f9e8e

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Agent commerce SDK for Python. The full merchant-side toolkit: identity gating + payment helpers + 402 builders + discovery + Stripe multichain. One install, submodule imports per concern.
44

5+
Every helper lifts directly from working production code (`agentscore/martin-estate`) — extract from real consumers, not speculation.
6+
57
## Submodules
68

79
| Submodule | What it is |
@@ -25,9 +27,23 @@ Single Python package, hatchling-built, published to PyPI as `agentscore-commerc
2527
| `agentscore_commerce/challenge/` | 402-body builders |
2628
| `agentscore_commerce/stripe_multichain/` | Stripe multichain PaymentIntent helpers |
2729
| `agentscore_commerce/api/` | `AgentScore` re-export |
30+
| `examples/` | Runnable single-file FastAPI apps for each common scenario |
2831
| `tests/` | pytest, one file per surface |
2932

30-
Every helper lifts from working production code (`agentscore/martin-estate`) — extract from real consumers, not speculation.
33+
Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime — vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm,svm]`, `pympp[server,tempo,stripe]`, `stripe`. Missing peer dep raises a guiding `ImportError` with the install command.
34+
35+
## Examples
36+
37+
`examples/` contains full single-file FastAPI apps for the most common merchant scenarios — copy-paste templates, not frameworks:
38+
39+
| Example | Scenario |
40+
|---|---|
41+
| `api_provider.py` | Per-call API billing on multiple rails: Tempo MPP + x402 (Base + Solana); no compliance gate |
42+
| `identity_only.py` | Compliance gate without payment (vendor handles their own) |
43+
| `multi_rail_merchant.py` | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
44+
| `stripe_multichain_merchant.py` | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
45+
| `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) |
46+
| `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`) |
3147

3248
## Identity model
3349

@@ -62,6 +78,10 @@ async def purchase(...): ...
6278

6379
Anonymous POST flows through to the handler unauthenticated and gets a 402 with all rails + per-order pricing. Identity is verified at settle time on the retry leg (when the agent submits `X-Payment` / `Authorization: Payment`); `create_session_on_missing` still auto-mints a verification session there. The same wrap pattern works identically across all 6 framework adapters (fastapi, flask, django, aiohttp, sanic, middleware/ASGI). See `examples/multi_rail_merchant.py` and `examples/compliance_merchant.py`.
6480

81+
### `compatible_clients` field on emitted 402s
82+
83+
`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay` — per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands.
84+
6585
## Tooling
6686

6787
- **uv** — package manager.

agentscore_commerce/challenge/agent_instructions.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ def _default_warnings(how_to_pay: dict[str, Any]) -> list[str]:
4646
return w
4747

4848

49+
def _default_compatible_clients(how_to_pay: dict[str, Any]) -> dict[str, list[str]] | None:
50+
"""Default ``compatible_clients`` derived from the rails declared in ``how_to_pay``.
51+
52+
Lists clients the AgentScore team has smoke-verified end-to-end against an
53+
``agentscore-commerce`` merchant; entries appear only for rails the vendor actually
54+
offers. Vendors override this in ``BuildAgentInstructionsInput(compatible_clients=...)``
55+
to add their own tested clients or remove entries that don't fit their endpoint.
56+
57+
Verified state as of the SDK release. The same data is also published as a docs page
58+
for humans (rationale, per-rail commands, why some clients don't fully work, last
59+
verified date) — this default keeps the merchant-side surface in sync.
60+
"""
61+
out: dict[str, list[str]] = {}
62+
if "tempo" in how_to_pay:
63+
out["tempo_mpp"] = ["agentscore-pay", "tempo request", "x402-proxy"]
64+
if "x402_base" in how_to_pay:
65+
out["x402_base"] = ["agentscore-pay", "x402-proxy", "purl (omit --network flag)"]
66+
if "x402_solana" in how_to_pay:
67+
out["x402_solana"] = ["agentscore-pay"]
68+
if "stripe" in how_to_pay:
69+
out["stripe"] = ["link-cli"]
70+
return out or None
71+
72+
4973
@dataclass
5074
class BuildAgentInstructionsInput:
5175
how_to_pay: dict[str, Any]
@@ -54,6 +78,11 @@ class BuildAgentInstructionsInput:
5478
timeout_seconds: int = 300
5579
warnings: list[str] | None = None
5680
recommended: str | None = None
81+
# Per-rail list of client names the merchant has verified work end-to-end.
82+
# Vendors set this from their own smoke matrix — defaults to None, in which case
83+
# the field is not emitted (avoids vouching for clients the merchant has not tested).
84+
# Keys are rail identifiers (e.g. "x402_base", "tempo_mpp"); values are display labels.
85+
compatible_clients: dict[str, list[str]] | None = None
5786
extra: dict[str, Any] = field(default_factory=dict)
5887

5988

@@ -68,6 +97,11 @@ def build_agent_instructions(input: BuildAgentInstructionsInput) -> dict[str, An
6897
input.recommended_tools if input.recommended_tools is not None else _default_recommended_tools(input.how_to_pay)
6998
)
7099
warnings = input.warnings if input.warnings is not None else _default_warnings(input.how_to_pay)
100+
compatible_clients = (
101+
input.compatible_clients
102+
if input.compatible_clients is not None
103+
else _default_compatible_clients(input.how_to_pay)
104+
)
71105
out: dict[str, Any] = {
72106
"how_to_pay": input.how_to_pay,
73107
"recommended_tools": recommended_tools,
@@ -77,5 +111,7 @@ def build_agent_instructions(input: BuildAgentInstructionsInput) -> dict[str, An
77111
}
78112
if input.recommended:
79113
out["recommended"] = input.recommended
114+
if compatible_clients:
115+
out["compatible_clients"] = compatible_clients
80116
out.update(input.extra)
81117
return out

0 commit comments

Comments
 (0)