Skip to content

Commit cea1cbe

Browse files
vvillait88claude
andcommitted
examples: align with node — full identity-only + real SDK helpers in
multi-rail/stripe-multichain/variable-cost - identity_only.py: add create_session_on_missing + capture_wallet + public route to match node's teaching depth. - multi_rail_merchant.py: replace _create_multichain_payment_intent stub with the real create_multichain_payment_intent helper + pi_cache writes. - stripe_multichain_merchant.py: rename POST /buy -> POST /checkout, add the 3-network instructions block (matches node prose). - variable_cost_merchant.py: swap raw 402 dicts for build_402_body + build_accepted_methods + build_agent_instructions + build_how_to_pay + build_pricing_block; wire create_x402_server/create_mppx_server hooks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7832a7b commit cea1cbe

4 files changed

Lines changed: 222 additions & 90 deletions

File tree

‎examples/identity_only.py‎

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,88 @@
1-
"""Example: compliance gate without payment
1+
"""Example: identity gate without payment
22
3-
Scenario: you sell something where the gating is the whole product — your service handles
4-
its own billing (Stripe, invoice, prepaid credit, etc.) but you need to verify the agent
5-
operator is KYC'd, age-verified, sanctions-clear, and in an allowed jurisdiction before
6-
delivering.
3+
Scenario: you have an existing checkout / payment flow you don't want to change,
4+
but you want to verify the agent is KYC'd before letting them transact. Use the
5+
commerce/identity middleware as a thin wrapper over your existing endpoints.
76
8-
This is the smallest possible commerce integration. Mount the gate, write your route,
9-
done. No 402 logic, no payment plumbing — just identity gating.
7+
Common cases:
8+
* Compliance-required content (age-gated, sanctioned-restricted)
9+
* High-value transactions where you want extra identity assurance
10+
* Adding agent KYC to an existing human-only Stripe checkout
11+
12+
This is the smallest possible commerce integration. Mount the gate, write your
13+
route, done. No 402 logic, no payment plumbing; just identity gating.
1014
1115
Peer deps:
1216
pip install agentscore-commerce[fastapi]
1317
1418
Env vars:
15-
AGENTSCORE_API_KEY — get one at agentscore.sh/dashboard
19+
AGENTSCORE_API_KEY — your AgentScore API key
1620
1721
Run: uvicorn examples.identity_only:app --port 3000
1822
"""
1923

20-
from fastapi import Depends, FastAPI
24+
from __future__ import annotations
25+
26+
import os
27+
from typing import Any
2128

22-
from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data
29+
from fastapi import Depends, FastAPI, Request
30+
31+
from agentscore_commerce.identity.fastapi import (
32+
AgentScoreGate,
33+
capture_wallet,
34+
get_agentscore_data,
35+
)
36+
from agentscore_commerce.identity.sessions import CreateSessionOnMissing
2337

2438
app = FastAPI()
2539

40+
API_KEY = os.environ.get("AGENTSCORE_API_KEY", "ask_test_dummy")
41+
42+
# ── Apply identity gate to specific routes ──────────────────────────────────
2643
gate = AgentScoreGate(
27-
api_key="ask_...", # use os.environ["AGENTSCORE_API_KEY"] in prod
44+
api_key=API_KEY,
2845
require_kyc=True,
2946
require_sanctions_clear=True,
3047
min_age=21,
3148
allowed_jurisdictions=["US"],
49+
# When the agent has no identity header, auto-create a verification session
50+
# so the 403 body carries verify_url + poll_secret + agent_instructions.
51+
create_session_on_missing=CreateSessionOnMissing(
52+
api_key=API_KEY,
53+
context="restricted-access",
54+
),
3255
)
3356

3457

35-
@app.post("/deliver", dependencies=[Depends(gate)])
36-
async def deliver(assess: dict = Depends(get_agentscore_data)):
58+
@app.post("/restricted", dependencies=[Depends(gate)])
59+
async def restricted(assess: dict[str, Any] = Depends(get_agentscore_data)) -> dict[str, Any]:
3760
"""Gated route — only reached when the agent passes the compliance policy.
3861
39-
`assess` is the raw `/v1/assess` response. Use it for downstream business logic that
40-
depends on the verified identity (audit trail, per-operator pricing, etc.).
62+
`assess` is the raw `/v1/assess` response: ``{ decision, operator,
63+
kyc_verified, age_bracket, jurisdiction, ... }``. Run your own business
64+
logic here; buy something via your existing Stripe flow, grant access to
65+
gated content, write to your DB, whatever. AgentScore's job ends at "this
66+
agent is verified, here's their operator id."
4167
"""
42-
return {"status": "delivered", "operator": assess.get("resolved_operator")}
68+
return {"ok": True, "operator": assess.get("resolved_operator")}
69+
70+
71+
# ── Optional: capture an agent's wallet after payment lands ────────────────
72+
# (only relevant if your downstream payment flow exposes the signer wallet)
73+
@app.post("/restricted/capture-wallet-example", dependencies=[Depends(gate)])
74+
async def capture_wallet_example(request: Request) -> dict[str, Any]:
75+
body = await request.json()
76+
await capture_wallet(
77+
request,
78+
wallet_address=body["signer_address"],
79+
network="evm",
80+
idempotency_key=body.get("payment_intent_id"),
81+
)
82+
return {"ok": True}
83+
84+
85+
# ── Public routes (no gate) ────────────────────────────────────────────────
86+
@app.get("/public-info")
87+
async def public_info() -> dict[str, str]:
88+
return {"message": "open access — no identity required"}

‎examples/multi_rail_merchant.py‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
validate_x402_network_config,
6666
)
6767
from agentscore_commerce.stripe_multichain import (
68+
create_multichain_payment_intent,
6869
create_pi_cache,
6970
simulate_deposit_if_test_mode,
7071
)
@@ -75,21 +76,17 @@
7576
SOLANA_NETWORK_CAIP2 = os.environ.get("SOLANA_NETWORK_CAIP2", networks.solana.mainnet.caip2)
7677
validate_x402_network_config(base_network=X402_BASE_NETWORK)
7778

79+
# Singleton Stripe client + PI / deposit-address cache. Redis-backed when
80+
# REDIS_URL is set (multi-task deployments need this so a deposit lands on
81+
# whichever task settles it).
82+
import stripe # noqa: E402 optional peer dep installed by the example user
83+
84+
stripe_client = stripe.StripeClient(STRIPE_SECRET_KEY)
7885
pi_cache = create_pi_cache(redis_url=os.environ.get("REDIS_URL"))
7986

8087
app = FastAPI()
8188

8289

83-
async def _create_multichain_payment_intent(_total_usd: str) -> dict[str, str]:
84-
"""Vendor's actual Stripe multichain PI mint call.
85-
86-
Returns deposit addresses for {tempo, base, solana}. In production this
87-
calls `stripe.PaymentIntent.create(...)` with `payment_method_types` set
88-
+ reads back the per-network deposit addresses Stripe minted.
89-
"""
90-
return {"tempo": "0x...", "base": "0x...", "solana": "..."}
91-
92-
9390
async def _validate_purchase(ctx: Any) -> dict[str, Any]:
9491
"""preValidate hook: shape-check the request body before pricing/gate runs."""
9592
body = ctx.request.body if isinstance(ctx.request.body, dict) else {}
@@ -109,12 +106,20 @@ async def _compute_pricing(ctx: Any) -> PricingResult:
109106

110107
async def _mint_recipients(ctx: Any) -> dict[str, str]:
111108
"""Per-order recipient mint: Stripe multichain PI → per-network deposit addresses."""
112-
total_usd = f"{ctx.pricing.amount_usd:.2f}"
113-
addresses = await _create_multichain_payment_intent(total_usd)
109+
total_cents = round(ctx.pricing.amount_usd * 100)
110+
result = create_multichain_payment_intent(
111+
stripe=stripe_client,
112+
amount=total_cents,
113+
networks=["tempo", "base", "solana"],
114+
)
115+
for addr in result.deposit_addresses.values():
116+
await pi_cache.cache_address(addr)
117+
pi_cache.cache_payment_intent(addr, result.payment_intent_id)
118+
pi_cache.cache_network_addresses(result.payment_intent_id, result.deposit_addresses)
114119
return {
115-
"tempo": addresses["tempo"],
116-
"x402_base": addresses["base"],
117-
"solana_mpp": addresses["solana"],
120+
"tempo": result.deposit_addresses["tempo"],
121+
"x402_base": result.deposit_addresses["base"],
122+
"solana_mpp": result.deposit_addresses["solana"],
118123
}
119124

120125

‎examples/stripe_multichain_merchant.py‎

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
"""Example: Stripe-anchored multichain merchant
22
3-
Scenario: you want to accept agent payments but settle through Stripe so all your existing
4-
billing/refund/dashboard infrastructure keeps working. Stripe issues a single PaymentIntent
5-
with deposit_options for tempo/base/solana — the agent picks any chain to send USDC, and
6-
Stripe auto-captures the PI when the deposit lands.
3+
Scenario: you want crypto payments but you're already a Stripe merchant. Use Stripe's
4+
``deposit_options`` to issue per-PI deposit addresses on multiple chains (Tempo, Base,
5+
Solana). Agent picks a chain and sends USDC to the matching address; Stripe auto-captures
6+
when funds land. Net: one Stripe PI per purchase, multi-chain optionality, settlement
7+
tracked in Stripe.
78
8-
Distinct from the Stripe SPT (Shared Payment Token) flow — this is the "agent sends crypto,
9-
Stripe handles settlement on your behalf" path.
9+
Distinct from Stripe SPT (Shared Payment Token), which is for user-approved cards via
10+
the ``link-cli`` flow. This example is the "merchant funds via crypto rails" path.
1011
1112
Peer deps:
12-
pip install agentscore-commerce[fastapi,stripe]
13+
pip install 'agentscore-commerce[fastapi,stripe]'
1314
1415
Env vars:
15-
STRIPE_SECRET_KEY — your sk_... secret key (sk_test_ for testnet)
16+
STRIPE_SECRET_KEY — sk_live_... or sk_test_...
1617
1718
Run: uvicorn examples.stripe_multichain_merchant:app --port 3000
1819
"""
@@ -33,37 +34,51 @@
3334
app = FastAPI()
3435

3536

36-
@app.post("/buy")
37-
async def buy(body: dict):
38-
# Create a multichain PaymentIntent — Stripe issues deposit addresses for each requested chain.
37+
@app.post("/checkout")
38+
async def checkout(body: dict) -> dict:
39+
amount_cents = round(float(body["amount_usd"]) * 100)
40+
41+
# 1. Create a Stripe PI with deposit addresses on tempo + base + solana.
3942
result = create_multichain_payment_intent(
4043
stripe=stripe_client,
41-
amount=body.get("amount_cents", 25000),
44+
amount=amount_cents,
4245
networks=["tempo", "base", "solana"],
43-
metadata={"order_id": body.get("order_id", "ord_demo"), "merchant": "example"},
44-
idempotency_key=body.get("order_id"),
46+
metadata={"order_id": body.get("order_id"), "merchant": "example-store"},
47+
idempotency_key=f"pi-{body['order_id']}-{amount_cents}" if body.get("order_id") else None,
4548
)
4649

50+
# 2. Return per-network deposit addresses to the agent (or 402 with
51+
# addresses embedded — see multi_rail_merchant.py for the full 402-builder
52+
# pattern).
53+
amount_usd = body["amount_usd"]
54+
tempo = result.deposit_addresses.get("tempo")
55+
base = result.deposit_addresses.get("base")
56+
solana = result.deposit_addresses.get("solana")
4757
return {
4858
"payment_intent_id": result.payment_intent_id,
4959
"deposit_addresses": result.deposit_addresses,
50-
"pay_to": {
51-
"base": result.deposit_addresses.get("base"),
52-
"tempo": result.deposit_addresses.get("tempo"),
60+
"instructions": {
61+
"tempo": (f"Send {amount_usd} USDC on Tempo to {tempo}" if tempo else "Tempo not available for this PI"),
62+
"base": (f"Send {amount_usd} USDC on Base to {base}" if base else "Base not available for this PI"),
63+
"solana": (
64+
f"Send {amount_usd} USDC on Solana to {solana}" if solana else "Solana not available for this PI"
65+
),
5366
},
5467
}
5568

5669

57-
# Testnet helper: simulate a deposit landing on a PI. Useful for end-to-end testing without
58-
# real on-chain transfers. For the typical "fire after PI mint if sk_test_" pattern, prefer
59-
# `simulate_deposit_if_test_mode` which gates internally — see multi_rail_merchant.py.
70+
# ── Testnet helper: simulate a deposit landing on a PI ──────────────────────
71+
# Useful for end-to-end testing without real on-chain transfers. For the
72+
# typical "fire after PI mint if sk_test_" pattern, prefer
73+
# `simulate_deposit_if_test_mode` which gates internally — see
74+
# multi_rail_merchant.py.
6075
@app.post("/testnet/simulate-deposit")
61-
async def simulate_deposit(body: dict):
76+
async def simulate_deposit(body: dict) -> dict:
6277
await simulate_crypto_deposit(
6378
payment_intent_id=body["payment_intent_id"],
6479
network=body["network"],
6580
stripe_secret_key=os.environ["STRIPE_SECRET_KEY"],
66-
stripe_version="2026-03-04.preview",
81+
stripe_version="2026-03-04.preview", # if you're on a preview API
6782
token_currency="usdc",
6883
transaction_hash=STRIPE_TEST_TX_HASH_SUCCESS,
6984
)

0 commit comments

Comments
 (0)