Skip to content

Commit a750b12

Browse files
vvillait88claude
andcommitted
docs(commerce): gate-conditional pattern in examples + README + CLAUDE
Mirror the node-commerce gate-conditional pattern in Python so merchants supporting non-AgentScore x402 wallets (Coinbase awal, Phantom, Solflare, ...) can return a real 402 challenge from anonymous discovery without an AgentScore credential. Identity verification fires on the retry leg when X-Payment / Authorization: Payment arrives. Updated: - examples/multi_rail_merchant.py: AgentScoreGate wrapped in `gate_on_settle` async dependency, used via `Depends(gate_on_settle)` - examples/compliance_merchant.py: same wrap with full `on_denied` branching preserved at settle - README.md: FastAPI quickstart shows the conditional wrap - CLAUDE.md: new "Mount posture: gate-first vs gate-conditional" section, notes the same wrap applies identically across all 6 framework adapters (fastapi, flask, django, aiohttp, sanic, middleware/ASGI) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7134604 commit a750b12

4 files changed

Lines changed: 78 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-
3939

4040
Captured wallets: `capture_wallet(...)` is fire-and-forget — reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests.
4141

42+
### Mount posture: gate-first vs gate-conditional
43+
44+
`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only — every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, …), wrap the gate so it fires only when a payment credential is attached:
45+
46+
```python
47+
_gate = AgentScoreGate(api_key=..., require_kyc=True, ...)
48+
49+
async def gate_on_settle(request: Request) -> None:
50+
has_payment_header = bool(
51+
request.headers.get("payment-signature")
52+
or request.headers.get("x-payment")
53+
or (request.headers.get("authorization") or "").startswith("Payment ")
54+
)
55+
if not has_payment_header:
56+
return None
57+
return await _gate(request)
58+
59+
@app.post("/purchase", dependencies=[Depends(gate_on_settle)])
60+
async def purchase(...): ...
61+
```
62+
63+
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`.
64+
4265
## Tooling
4366

4467
- **uv** — package manager.

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,30 @@ from agentscore_commerce.identity.fastapi import (
3535
)
3636

3737
app = FastAPI()
38-
gate = AgentScoreGate(
38+
_gate = AgentScoreGate(
3939
api_key="as_live_...",
4040
require_kyc=True,
4141
min_age=21,
4242
allowed_jurisdictions=["US"],
4343
)
4444

45-
@app.post("/purchase", dependencies=[Depends(gate)])
45+
46+
# Run the gate CONDITIONALLY — only when a payment credential is already attached.
47+
# Anonymous discovery (no payment header) flows through to the handler so any spec-
48+
# compliant x402 wallet can read the 402 challenge with rails + pricing without first
49+
# proving identity. Identity is verified at settle time on the retry leg.
50+
async def gate_on_settle(request: Request) -> None:
51+
has_payment_header = bool(
52+
request.headers.get("payment-signature")
53+
or request.headers.get("x-payment")
54+
or (request.headers.get("authorization") or "").startswith("Payment ")
55+
)
56+
if not has_payment_header:
57+
return None
58+
return await _gate(request)
59+
60+
61+
@app.post("/purchase", dependencies=[Depends(gate_on_settle)])
4662
async def purchase(request: Request, assess=Depends(get_assess_data)):
4763
# ... settle payment ...
4864
# After payment, capture the signer wallet for cross-merchant attribution

examples/compliance_merchant.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def _on_denied(_request: Request, reason: DenialReason) -> tuple[dict[str, Any],
100100

101101

102102
app = FastAPI()
103-
gate = AgentScoreGate(
103+
_gate = AgentScoreGate(
104104
api_key=os.environ["AGENTSCORE_API_KEY"],
105105
require_kyc=True,
106106
require_sanctions_clear=True,
@@ -110,7 +110,22 @@ def _on_denied(_request: Request, reason: DenialReason) -> tuple[dict[str, Any],
110110
)
111111

112112

113-
@app.post("/buy", dependencies=[Depends(gate)])
113+
# Conditional gate. Fires only when a payment credential is already attached so
114+
# anonymous discovery returns a 402 challenge (not a 403 missing_identity). Compliance
115+
# gating + signer-match still run on the retry leg when X-Payment / Authorization:
116+
# Payment arrives — the full denial branching above triggers there.
117+
async def gate_on_settle(request: Request) -> None:
118+
has_payment_header = bool(
119+
request.headers.get("payment-signature")
120+
or request.headers.get("x-payment")
121+
or (request.headers.get("authorization") or "").startswith("Payment ")
122+
)
123+
if not has_payment_header:
124+
return None
125+
return await _gate(request)
126+
127+
128+
@app.post("/buy", dependencies=[Depends(gate_on_settle)])
114129
async def buy(request: Request, assess: dict = Depends(get_assess_data)):
115130
# Wallet-auth: verify the payment signer matches the claimed wallet (or a same-operator
116131
# linked wallet). No-ops for operator_token requests. Pass `signer=` from your real x402/MPP

examples/multi_rail_merchant.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,20 +93,38 @@
9393
pi_cache = create_pi_cache(PiCacheOptions(redis_url=os.environ.get("REDIS_URL")))
9494

9595
app = FastAPI()
96-
gate = AgentScoreGate(
96+
_gate = AgentScoreGate(
9797
api_key=os.environ["AGENTSCORE_API_KEY"],
9898
require_kyc=True,
9999
require_sanctions_clear=True,
100100
min_age=21,
101101
allowed_jurisdictions=["US"],
102102
)
103103

104+
105+
# Conditional gate: fires only when a payment credential is already attached. Anonymous
106+
# requests (no payment header) fall through to the handler unauthenticated and receive
107+
# a clean 402 with all rails advertised — so any spec-compliant x402 wallet (Coinbase
108+
# awal, Phantom, Solflare, etc.) can discover prices before AgentScore identity exists.
109+
# Identity is verified at settle time (when X-Payment / Authorization: Payment arrives),
110+
# and `create_session_on_missing` then auto-mints a verification session.
111+
async def gate_on_settle(request: Request) -> None:
112+
has_payment_header = bool(
113+
request.headers.get("payment-signature")
114+
or request.headers.get("x-payment")
115+
or (request.headers.get("authorization") or "").startswith("Payment ")
116+
)
117+
if not has_payment_header:
118+
return None
119+
return await _gate(request)
120+
121+
104122
# Vendor-instantiated x402 server + pympp server are stubs in this example —
105123
# replace with your `create_x402_server(...)` + `create_mppx_server(...)` setup.
106124
x402_server: object = ... # type: ignore[assignment]
107125

108126

109-
@app.post("/purchase", dependencies=[Depends(gate)])
127+
@app.post("/purchase", dependencies=[Depends(gate_on_settle)])
110128
async def purchase(request: Request, assess: dict = Depends(get_assess_data)):
111129
body = await request.json()
112130

0 commit comments

Comments
 (0)