agentscore-commerce 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentscore_commerce/__init__.py +12 -0
- agentscore_commerce/api/__init__.py +23 -0
- agentscore_commerce/challenge/__init__.py +86 -0
- agentscore_commerce/challenge/accepted_methods.py +90 -0
- agentscore_commerce/challenge/agent_instructions.py +117 -0
- agentscore_commerce/challenge/agent_memory.py +50 -0
- agentscore_commerce/challenge/body.py +67 -0
- agentscore_commerce/challenge/how_to_pay.py +199 -0
- agentscore_commerce/challenge/identity.py +42 -0
- agentscore_commerce/challenge/order_receipt.py +87 -0
- agentscore_commerce/challenge/pricing.py +113 -0
- agentscore_commerce/challenge/respond_402.py +74 -0
- agentscore_commerce/challenge/validation_error.py +71 -0
- agentscore_commerce/discovery/__init__.py +72 -0
- agentscore_commerce/discovery/bazaar.py +30 -0
- agentscore_commerce/discovery/llms_txt.py +278 -0
- agentscore_commerce/discovery/openapi.py +142 -0
- agentscore_commerce/discovery/probe.py +192 -0
- agentscore_commerce/discovery/robots_tag.py +190 -0
- agentscore_commerce/discovery/well_known_mpp.py +64 -0
- agentscore_commerce/identity/__init__.py +124 -0
- agentscore_commerce/identity/_denial.py +201 -0
- agentscore_commerce/identity/_response.py +226 -0
- agentscore_commerce/identity/a2a.py +178 -0
- agentscore_commerce/identity/address.py +35 -0
- agentscore_commerce/identity/aiohttp.py +260 -0
- agentscore_commerce/identity/cache.py +51 -0
- agentscore_commerce/identity/client.py +553 -0
- agentscore_commerce/identity/django.py +244 -0
- agentscore_commerce/identity/fastapi.py +284 -0
- agentscore_commerce/identity/flask.py +295 -0
- agentscore_commerce/identity/middleware.py +284 -0
- agentscore_commerce/identity/policy.py +182 -0
- agentscore_commerce/identity/py.typed +0 -0
- agentscore_commerce/identity/sanic.py +261 -0
- agentscore_commerce/identity/sessions.py +239 -0
- agentscore_commerce/identity/signer.py +65 -0
- agentscore_commerce/identity/types.py +281 -0
- agentscore_commerce/identity/ucp.py +241 -0
- agentscore_commerce/payment/__init__.py +135 -0
- agentscore_commerce/payment/directive.py +111 -0
- agentscore_commerce/payment/dispatch.py +35 -0
- agentscore_commerce/payment/headers.py +165 -0
- agentscore_commerce/payment/idempotency.py +71 -0
- agentscore_commerce/payment/mppx_server.py +199 -0
- agentscore_commerce/payment/networks.py +64 -0
- agentscore_commerce/payment/rails.py +93 -0
- agentscore_commerce/payment/settlement_override.py +23 -0
- agentscore_commerce/payment/signer.py +100 -0
- agentscore_commerce/payment/usdc.py +57 -0
- agentscore_commerce/payment/wwwauthenticate.py +59 -0
- agentscore_commerce/payment/x402.py +30 -0
- agentscore_commerce/payment/x402_server.py +201 -0
- agentscore_commerce/payment/x402_settle.py +119 -0
- agentscore_commerce/payment/x402_validation.py +225 -0
- agentscore_commerce/stripe_multichain/__init__.py +47 -0
- agentscore_commerce/stripe_multichain/mppx_stripe.py +69 -0
- agentscore_commerce/stripe_multichain/payment_intent.py +90 -0
- agentscore_commerce/stripe_multichain/pi_cache.py +197 -0
- agentscore_commerce/stripe_multichain/simulate_deposit.py +127 -0
- agentscore_commerce-1.0.0.dist-info/METADATA +349 -0
- agentscore_commerce-1.0.0.dist-info/RECORD +64 -0
- agentscore_commerce-1.0.0.dist-info/WHEEL +4 -0
- agentscore_commerce-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Agent commerce SDK — identity middleware + payment helpers + 402 builders + discovery + Stripe multichain.
|
|
2
|
+
|
|
3
|
+
Submodules:
|
|
4
|
+
agentscore_commerce.identity - per-framework gate adapters
|
|
5
|
+
agentscore_commerce.payment - networks/usdc/rails registries, directives, x402/mpp helpers
|
|
6
|
+
agentscore_commerce.discovery - probe + .well-known/mpp.json + llms.txt + OpenAPI snippets
|
|
7
|
+
agentscore_commerce.challenge - 402-body builders
|
|
8
|
+
agentscore_commerce.stripe_multichain - multichain PaymentIntent helpers
|
|
9
|
+
agentscore_commerce.api - AgentScore SDK re-export
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""AgentScore SDK re-export — single import path for the underlying agentscore-py.
|
|
2
|
+
|
|
3
|
+
Vendors install only ``agentscore-commerce`` and reach everything from the underlying
|
|
4
|
+
``agentscore-py`` here. Don't add ``agentscore-py`` as a separate dep; the two can
|
|
5
|
+
drift versions and cause subtle type mismatches.
|
|
6
|
+
|
|
7
|
+
Use this for: programmatic API calls (sessions, credentials, reputation) and the
|
|
8
|
+
test-mode address fixtures for integration tests.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from agentscore import (
|
|
12
|
+
AGENTSCORE_TEST_ADDRESSES,
|
|
13
|
+
AgentScore,
|
|
14
|
+
AgentScoreError,
|
|
15
|
+
is_agentscore_test_address,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AGENTSCORE_TEST_ADDRESSES",
|
|
20
|
+
"AgentScore",
|
|
21
|
+
"AgentScoreError",
|
|
22
|
+
"is_agentscore_test_address",
|
|
23
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""402-body builders + pricing/receipt/agent-memory helpers."""
|
|
2
|
+
|
|
3
|
+
from agentscore_commerce.challenge.accepted_methods import (
|
|
4
|
+
BuildAcceptedMethodsInput,
|
|
5
|
+
StripeConfig,
|
|
6
|
+
TempoConfig,
|
|
7
|
+
X402BaseConfig,
|
|
8
|
+
X402SolanaConfig,
|
|
9
|
+
build_accepted_methods,
|
|
10
|
+
)
|
|
11
|
+
from agentscore_commerce.challenge.agent_instructions import (
|
|
12
|
+
BuildAgentInstructionsInput,
|
|
13
|
+
build_agent_instructions,
|
|
14
|
+
)
|
|
15
|
+
from agentscore_commerce.challenge.agent_memory import (
|
|
16
|
+
AgentMemoryHint,
|
|
17
|
+
build_agent_memory_hint,
|
|
18
|
+
first_encounter_agent_memory,
|
|
19
|
+
)
|
|
20
|
+
from agentscore_commerce.challenge.body import Build402BodyInput, X402PaymentRequired, build_402_body
|
|
21
|
+
from agentscore_commerce.challenge.how_to_pay import (
|
|
22
|
+
BuildHowToPayInput,
|
|
23
|
+
HowToPayRails,
|
|
24
|
+
StripeRailConfig,
|
|
25
|
+
TempoRailConfig,
|
|
26
|
+
X402BaseRailConfig,
|
|
27
|
+
X402SolanaRailConfig,
|
|
28
|
+
build_how_to_pay,
|
|
29
|
+
)
|
|
30
|
+
from agentscore_commerce.challenge.identity import (
|
|
31
|
+
IdentityMetadataInput,
|
|
32
|
+
IdentityMode,
|
|
33
|
+
SignerMatchResult,
|
|
34
|
+
build_identity_metadata,
|
|
35
|
+
)
|
|
36
|
+
from agentscore_commerce.challenge.order_receipt import (
|
|
37
|
+
OrderNextSteps,
|
|
38
|
+
OrderProductInfo,
|
|
39
|
+
OrderReceipt,
|
|
40
|
+
ShippingAddress,
|
|
41
|
+
)
|
|
42
|
+
from agentscore_commerce.challenge.pricing import PricingBlock, build_pricing_block
|
|
43
|
+
from agentscore_commerce.challenge.respond_402 import Respond402Input, Respond402Result, respond_402
|
|
44
|
+
from agentscore_commerce.challenge.validation_error import (
|
|
45
|
+
BuildValidationErrorInput,
|
|
46
|
+
build_validation_error,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"AgentMemoryHint",
|
|
51
|
+
"Build402BodyInput",
|
|
52
|
+
"BuildAcceptedMethodsInput",
|
|
53
|
+
"BuildAgentInstructionsInput",
|
|
54
|
+
"BuildHowToPayInput",
|
|
55
|
+
"BuildValidationErrorInput",
|
|
56
|
+
"HowToPayRails",
|
|
57
|
+
"IdentityMetadataInput",
|
|
58
|
+
"IdentityMode",
|
|
59
|
+
"OrderNextSteps",
|
|
60
|
+
"OrderProductInfo",
|
|
61
|
+
"OrderReceipt",
|
|
62
|
+
"PricingBlock",
|
|
63
|
+
"Respond402Input",
|
|
64
|
+
"Respond402Result",
|
|
65
|
+
"ShippingAddress",
|
|
66
|
+
"SignerMatchResult",
|
|
67
|
+
"StripeConfig",
|
|
68
|
+
"StripeRailConfig",
|
|
69
|
+
"TempoConfig",
|
|
70
|
+
"TempoRailConfig",
|
|
71
|
+
"X402BaseConfig",
|
|
72
|
+
"X402BaseRailConfig",
|
|
73
|
+
"X402PaymentRequired",
|
|
74
|
+
"X402SolanaConfig",
|
|
75
|
+
"X402SolanaRailConfig",
|
|
76
|
+
"build_402_body",
|
|
77
|
+
"build_accepted_methods",
|
|
78
|
+
"build_agent_instructions",
|
|
79
|
+
"build_agent_memory_hint",
|
|
80
|
+
"build_how_to_pay",
|
|
81
|
+
"build_identity_metadata",
|
|
82
|
+
"build_pricing_block",
|
|
83
|
+
"build_validation_error",
|
|
84
|
+
"first_encounter_agent_memory",
|
|
85
|
+
"respond_402",
|
|
86
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""accepted_methods[] builder for enriched 402 bodies."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TempoConfig:
|
|
9
|
+
recipient: str
|
|
10
|
+
network: str = "tempo-mainnet"
|
|
11
|
+
chain_id: int = 4217
|
|
12
|
+
token: str = "0x20C000000000000000000000b9537d11c60E8b50"
|
|
13
|
+
symbol: str = "USDC.e"
|
|
14
|
+
decimals: int = 6
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class X402BaseConfig:
|
|
19
|
+
recipient: str
|
|
20
|
+
network: str = "eip155:8453"
|
|
21
|
+
chain_id: int = 8453
|
|
22
|
+
token: str = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
23
|
+
symbol: str = "USDC"
|
|
24
|
+
decimals: int = 6
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class X402SolanaConfig:
|
|
29
|
+
recipient: str
|
|
30
|
+
network: str = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
|
31
|
+
token: str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
32
|
+
symbol: str = "USDC"
|
|
33
|
+
decimals: int = 6
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class StripeConfig:
|
|
38
|
+
profile_id: str | None = None
|
|
39
|
+
rails: list[str] = field(default_factory=lambda: ["card", "link", "shared_payment_token"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class BuildAcceptedMethodsInput:
|
|
44
|
+
tempo: TempoConfig | None = None
|
|
45
|
+
x402_base: X402BaseConfig | None = None
|
|
46
|
+
x402_solana: X402SolanaConfig | None = None
|
|
47
|
+
stripe: StripeConfig | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_accepted_methods(input: BuildAcceptedMethodsInput) -> list[dict[str, Any]]:
|
|
51
|
+
"""Build the accepted_methods[] array. Each rail entry conditionally included if vendor passed it."""
|
|
52
|
+
out: list[dict[str, Any]] = []
|
|
53
|
+
if input.tempo:
|
|
54
|
+
out.append(
|
|
55
|
+
{
|
|
56
|
+
"method": "tempo/charge",
|
|
57
|
+
"network": input.tempo.network,
|
|
58
|
+
"chain_id": input.tempo.chain_id,
|
|
59
|
+
"token": input.tempo.token,
|
|
60
|
+
"symbol": input.tempo.symbol,
|
|
61
|
+
"decimals": input.tempo.decimals,
|
|
62
|
+
"pay_to": input.tempo.recipient,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
if input.x402_base:
|
|
66
|
+
out.append(
|
|
67
|
+
{
|
|
68
|
+
"method": "x402/exact",
|
|
69
|
+
"network": input.x402_base.network,
|
|
70
|
+
"chain_id": input.x402_base.chain_id,
|
|
71
|
+
"token": input.x402_base.token,
|
|
72
|
+
"symbol": input.x402_base.symbol,
|
|
73
|
+
"decimals": input.x402_base.decimals,
|
|
74
|
+
"pay_to": input.x402_base.recipient,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
if input.x402_solana:
|
|
78
|
+
out.append(
|
|
79
|
+
{
|
|
80
|
+
"method": "x402/exact",
|
|
81
|
+
"network": input.x402_solana.network,
|
|
82
|
+
"token": input.x402_solana.token,
|
|
83
|
+
"symbol": input.x402_solana.symbol,
|
|
84
|
+
"decimals": input.x402_solana.decimals,
|
|
85
|
+
"pay_to": input.x402_solana.recipient,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
if input.stripe:
|
|
89
|
+
out.append({"method": "stripe/charge", "rails": input.stripe.rails, "profile_id": input.stripe.profile_id})
|
|
90
|
+
return out
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""agent_instructions block builder for the 402 body."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
_TEMPO_WARNING = (
|
|
7
|
+
"Do NOT use `tempo wallet transfer` to pay to the address above. That moves USDC on-chain but does not "
|
|
8
|
+
"notify this server, leaving your order in pending_identity state. Use `tempo request` instead — it performs "
|
|
9
|
+
"the full MPP handshake (signs, submits Authorization: Payment, waits for server confirmation)."
|
|
10
|
+
)
|
|
11
|
+
_X402_WARNING = (
|
|
12
|
+
"Do NOT send USDC manually to the x402 deposit addresses (e.g. via a bare wallet `transfer`). Use "
|
|
13
|
+
"`agentscore-pay pay` so the X-Payment credential is signed and submitted; otherwise the order stays in "
|
|
14
|
+
"pending_identity even though the deposit lands."
|
|
15
|
+
)
|
|
16
|
+
_TEMPO_TOOL = "`tempo request` for Tempo USDC (installs via `tempo add request`)"
|
|
17
|
+
_AGENTSCORE_PAY_TOOL = (
|
|
18
|
+
"`agentscore-pay` (npm: `@agent-score/pay`) — single CLI for x402 on Base + Solana, "
|
|
19
|
+
"also speaks tempo MPP via `--chain tempo`"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DEFAULT_WALLET_COMPATIBILITY = (
|
|
23
|
+
"No specific wallet stack required. The 402 challenge is rail-neutral: any client that can produce a valid "
|
|
24
|
+
"MPP credential (Authorization: Payment) or x402 X-Payment header is accepted. The CLI commands above are "
|
|
25
|
+
"the easiest path; sign-it-yourself is fine too."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _default_recommended_tools(how_to_pay: dict[str, Any]) -> list[str]:
|
|
30
|
+
tools: list[str] = []
|
|
31
|
+
has_tempo = "tempo" in how_to_pay
|
|
32
|
+
has_x402 = "x402_base" in how_to_pay or "x402_solana" in how_to_pay
|
|
33
|
+
if has_tempo:
|
|
34
|
+
tools.append(_TEMPO_TOOL)
|
|
35
|
+
if has_tempo or has_x402:
|
|
36
|
+
tools.append(_AGENTSCORE_PAY_TOOL)
|
|
37
|
+
return tools
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _default_warnings(how_to_pay: dict[str, Any]) -> list[str]:
|
|
41
|
+
w: list[str] = []
|
|
42
|
+
if "tempo" in how_to_pay:
|
|
43
|
+
w.append(_TEMPO_WARNING)
|
|
44
|
+
if "x402_base" in how_to_pay or "x402_solana" in how_to_pay:
|
|
45
|
+
w.append(_X402_WARNING)
|
|
46
|
+
return w
|
|
47
|
+
|
|
48
|
+
|
|
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
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class BuildAgentInstructionsInput:
|
|
75
|
+
how_to_pay: dict[str, Any]
|
|
76
|
+
recommended_tools: list[str] | None = None
|
|
77
|
+
wallet_compatibility: str | None = None
|
|
78
|
+
timeout_seconds: int = 300
|
|
79
|
+
warnings: list[str] | None = None
|
|
80
|
+
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
|
|
86
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_agent_instructions(input: BuildAgentInstructionsInput) -> dict[str, Any]:
|
|
90
|
+
"""Build the agent_instructions block — combines how_to_pay with tools, warnings, compat note, timeout.
|
|
91
|
+
|
|
92
|
+
Defaults adapt to the rails declared in ``how_to_pay``: only tempo-relevant warnings/tools
|
|
93
|
+
appear if ``how_to_pay["tempo"]`` is set, only x402-relevant ones if ``x402_base``/
|
|
94
|
+
``x402_solana`` are set. Vendors override ``warnings``/``recommended_tools`` for full control.
|
|
95
|
+
"""
|
|
96
|
+
recommended_tools = (
|
|
97
|
+
input.recommended_tools if input.recommended_tools is not None else _default_recommended_tools(input.how_to_pay)
|
|
98
|
+
)
|
|
99
|
+
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
|
+
)
|
|
105
|
+
out: dict[str, Any] = {
|
|
106
|
+
"how_to_pay": input.how_to_pay,
|
|
107
|
+
"recommended_tools": recommended_tools,
|
|
108
|
+
"wallet_compatibility": input.wallet_compatibility or DEFAULT_WALLET_COMPATIBILITY,
|
|
109
|
+
"timeout_seconds": input.timeout_seconds,
|
|
110
|
+
"warnings": warnings,
|
|
111
|
+
}
|
|
112
|
+
if input.recommended:
|
|
113
|
+
out["recommended"] = input.recommended
|
|
114
|
+
if compatible_clients:
|
|
115
|
+
out["compatible_clients"] = compatible_clients
|
|
116
|
+
out.update(input.extra)
|
|
117
|
+
return out
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Helpers for emitting the cross-merchant ``agent_memory`` hint on merchant 402 responses.
|
|
2
|
+
|
|
3
|
+
The gate (:mod:`agentscore_commerce.identity`) emits ``agent_memory`` on identity-related
|
|
4
|
+
responses (sessions, credentials, missing_identity bootstraps). Merchants can ALSO include
|
|
5
|
+
the hint in their own 402 challenge bodies on first-encounter requests so agents persist
|
|
6
|
+
the cross-merchant pattern even when entering the ecosystem through a merchant-side
|
|
7
|
+
endpoint rather than a direct AgentScore API call.
|
|
8
|
+
|
|
9
|
+
Usage pattern:
|
|
10
|
+
- Merchant tracks per-operator (or per-IP / per-fingerprint) "have I seen this agent
|
|
11
|
+
before?" in their own DB
|
|
12
|
+
- On first encounter, include the hint so the agent saves the pattern
|
|
13
|
+
- On subsequent encounters, skip — the agent already has it (or never will)
|
|
14
|
+
|
|
15
|
+
The hint contents come from :func:`build_agent_memory_hint` (re-exported here for
|
|
16
|
+
convenience). Keep it stateless: AgentScore's pattern doesn't depend on the merchant's
|
|
17
|
+
identity, so every merchant emits the same shape.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from agentscore_commerce.identity.types import AgentMemoryHint, build_agent_memory_hint
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def first_encounter_agent_memory(
|
|
26
|
+
first_encounter: bool,
|
|
27
|
+
) -> AgentMemoryHint | None:
|
|
28
|
+
"""Return the ``agent_memory`` hint when this is a first encounter, otherwise ``None``.
|
|
29
|
+
|
|
30
|
+
Use directly with the ``agent_memory`` field of :func:`build_402_body`::
|
|
31
|
+
|
|
32
|
+
body = build_402_body(Build402BodyInput(
|
|
33
|
+
accepted_methods=accepted,
|
|
34
|
+
agent_instructions=instructions,
|
|
35
|
+
pricing=pricing,
|
|
36
|
+
agent_memory=first_encounter_agent_memory(
|
|
37
|
+
first_encounter=not has_seen_operator(operator_token),
|
|
38
|
+
),
|
|
39
|
+
))
|
|
40
|
+
|
|
41
|
+
Returning ``None`` means ``build_402_body`` cleanly skips the field instead of
|
|
42
|
+
emitting ``agent_memory: null`` (which would imply "I tried but failed" rather than
|
|
43
|
+
"didn't apply").
|
|
44
|
+
"""
|
|
45
|
+
if not first_encounter:
|
|
46
|
+
return None
|
|
47
|
+
return build_agent_memory_hint()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["AgentMemoryHint", "build_agent_memory_hint", "first_encounter_agent_memory"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""build_402_body — full enriched 402 response body builder."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field, is_dataclass
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from agentscore_commerce.challenge.pricing import PricingBlock
|
|
7
|
+
from agentscore_commerce.payment.wwwauthenticate import alias_amount_fields
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class X402PaymentRequired:
|
|
12
|
+
accepts: list[Any]
|
|
13
|
+
version: Literal[1, 2] = 2
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Build402BodyInput:
|
|
18
|
+
accepted_methods: list[dict[str, Any]]
|
|
19
|
+
agent_instructions: dict[str, Any] | None = None
|
|
20
|
+
identity_metadata: dict[str, Any] | None = None
|
|
21
|
+
agent_memory: Any = None
|
|
22
|
+
pricing: PricingBlock | None = None
|
|
23
|
+
amount_usd: str | None = None
|
|
24
|
+
currency: str | None = None
|
|
25
|
+
order_id: str | None = None
|
|
26
|
+
product: dict[str, str] | None = None
|
|
27
|
+
retry_body: Any = None
|
|
28
|
+
recommended: str | None = None
|
|
29
|
+
x402: X402PaymentRequired | None = None
|
|
30
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_402_body(input: Build402BodyInput) -> dict[str, Any]:
|
|
34
|
+
"""Assemble the full enriched 402 response body. Each section conditionally included if vendor passed it."""
|
|
35
|
+
body: dict[str, Any] = {"payment_required": True, "accepted_methods": input.accepted_methods}
|
|
36
|
+
if input.x402:
|
|
37
|
+
body["x402Version"] = input.x402.version
|
|
38
|
+
body["accepts"] = alias_amount_fields(input.x402.accepts)
|
|
39
|
+
if input.amount_usd is not None:
|
|
40
|
+
body["amount_usd"] = input.amount_usd
|
|
41
|
+
if input.currency:
|
|
42
|
+
body["currency"] = input.currency
|
|
43
|
+
if input.pricing:
|
|
44
|
+
body["pricing"] = input.pricing.to_dict()
|
|
45
|
+
if input.order_id is not None:
|
|
46
|
+
body["order_id"] = input.order_id
|
|
47
|
+
if input.product:
|
|
48
|
+
body["product"] = input.product
|
|
49
|
+
if input.recommended:
|
|
50
|
+
body["recommended"] = input.recommended
|
|
51
|
+
if input.retry_body is not None:
|
|
52
|
+
body["retry_body"] = input.retry_body
|
|
53
|
+
if input.identity_metadata:
|
|
54
|
+
body.update(input.identity_metadata)
|
|
55
|
+
if input.agent_instructions:
|
|
56
|
+
body["agent_instructions"] = input.agent_instructions
|
|
57
|
+
if input.agent_memory is not None:
|
|
58
|
+
# AgentMemoryHint is a dataclass; merchants pass it directly via
|
|
59
|
+
# first_encounter_agent_memory(...). Convert here so JSONResponse /
|
|
60
|
+
# json.dumps can serialise without per-merchant boilerplate.
|
|
61
|
+
body["agent_memory"] = (
|
|
62
|
+
asdict(input.agent_memory)
|
|
63
|
+
if is_dataclass(input.agent_memory) and not isinstance(input.agent_memory, type)
|
|
64
|
+
else input.agent_memory
|
|
65
|
+
)
|
|
66
|
+
body.update(input.extra)
|
|
67
|
+
return body
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""how_to_pay block builder — per-rail setup/command/what_it_does for 402 agent_instructions."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class TempoRailConfig:
|
|
10
|
+
recipient: str
|
|
11
|
+
network_name: str = "tempo-mainnet"
|
|
12
|
+
chain_id: int = 4217
|
|
13
|
+
recommend: Literal["tempo", "agentscore-pay", "both"] = "both"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class X402BaseRailConfig:
|
|
18
|
+
recipient: str
|
|
19
|
+
network: str = "eip155:8453"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class X402SolanaRailConfig:
|
|
24
|
+
recipient: str
|
|
25
|
+
network: str = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class StripeRailConfig:
|
|
30
|
+
profile_id: str | None = None
|
|
31
|
+
product_name: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class HowToPayRails:
|
|
36
|
+
tempo: TempoRailConfig | None = None
|
|
37
|
+
x402_base: X402BaseRailConfig | None = None
|
|
38
|
+
x402_solana: X402SolanaRailConfig | None = None
|
|
39
|
+
stripe: StripeRailConfig | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class BuildHowToPayInput:
|
|
44
|
+
url: str
|
|
45
|
+
retry_body_json: str
|
|
46
|
+
total_usd: float | str
|
|
47
|
+
rails: HowToPayRails
|
|
48
|
+
op_token_placeholder: str = "<your_opc_token>"
|
|
49
|
+
max_spend: float | str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
TEMPO_SETUP = [
|
|
53
|
+
"curl -fsSL https://tempo.xyz/install | bash",
|
|
54
|
+
"tempo wallet login",
|
|
55
|
+
"tempo wallet whoami",
|
|
56
|
+
"tempo wallet fund # if balance is zero",
|
|
57
|
+
]
|
|
58
|
+
PAY_SETUP_BASE = [
|
|
59
|
+
"npm install -g @agent-score/pay # or: brew install agentscore/tap/agentscore-pay",
|
|
60
|
+
"agentscore-pay wallet create --chain base",
|
|
61
|
+
"agentscore-pay balance --chain base # fund the printed address with USDC on Base",
|
|
62
|
+
]
|
|
63
|
+
PAY_SETUP_SOLANA = [
|
|
64
|
+
"npm install -g @agent-score/pay # or: brew install agentscore/tap/agentscore-pay",
|
|
65
|
+
"agentscore-pay wallet create --chain solana",
|
|
66
|
+
"agentscore-pay balance --chain solana # fund the printed address with USDC on Solana",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_how_to_pay(input: BuildHowToPayInput) -> dict[str, Any]:
|
|
71
|
+
"""Build the agent_instructions.how_to_pay block.
|
|
72
|
+
|
|
73
|
+
Generates per-rail setup/command/what_it_does so agents see concrete commands per rail in the 402 body.
|
|
74
|
+
"""
|
|
75
|
+
total_num = float(input.total_usd) if isinstance(input.total_usd, str) else input.total_usd
|
|
76
|
+
max_spend = str(input.max_spend) if input.max_spend is not None else f"{math.ceil(total_num) + 1:.2f}"
|
|
77
|
+
op_token = input.op_token_placeholder
|
|
78
|
+
block: dict[str, Any] = {}
|
|
79
|
+
|
|
80
|
+
if input.rails.tempo:
|
|
81
|
+
t = input.rails.tempo
|
|
82
|
+
tempo_command = (
|
|
83
|
+
f"tempo request -X POST -H 'X-Operator-Token: {op_token}' -H 'Content-Type: application/json' "
|
|
84
|
+
f"--json '{input.retry_body_json}' --max-spend {max_spend} {input.url}"
|
|
85
|
+
)
|
|
86
|
+
pay_command = (
|
|
87
|
+
f"agentscore-pay pay POST {input.url} --chain tempo -H 'X-Operator-Token: {op_token}' "
|
|
88
|
+
f"-H 'Content-Type: application/json' -d '{input.retry_body_json}' --max-spend {max_spend}"
|
|
89
|
+
)
|
|
90
|
+
entry: dict[str, Any] = {
|
|
91
|
+
"setup": TEMPO_SETUP,
|
|
92
|
+
"prerequisite": (
|
|
93
|
+
f"Run `tempo wallet whoami` and confirm USDC.e balance on {t.network_name} (chain {t.chain_id}) "
|
|
94
|
+
f"is at least ${max_spend}. If the tempo CLI is not installed, run the setup commands above first."
|
|
95
|
+
),
|
|
96
|
+
"command": pay_command if t.recommend == "agentscore-pay" else tempo_command,
|
|
97
|
+
"what_it_does": (
|
|
98
|
+
f"Hits this endpoint, receives this same 402, signs the MPP challenge on {t.network_name}, and "
|
|
99
|
+
"submits the credential back via Authorization: Payment. Either client (tempo request or "
|
|
100
|
+
"agentscore-pay pay --chain tempo) works — both run the full MPP handshake."
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
if t.recommend == "both":
|
|
104
|
+
entry["alternative_command"] = pay_command
|
|
105
|
+
elif t.recommend == "agentscore-pay":
|
|
106
|
+
entry["alternative_command"] = tempo_command
|
|
107
|
+
block["tempo"] = entry
|
|
108
|
+
|
|
109
|
+
if input.rails.x402_base:
|
|
110
|
+
b = input.rails.x402_base
|
|
111
|
+
block["x402_base"] = {
|
|
112
|
+
"setup": PAY_SETUP_BASE,
|
|
113
|
+
"prerequisite": (
|
|
114
|
+
f"Run `agentscore-pay balance --chain base` and confirm USDC balance on Base ({b.network}) is at "
|
|
115
|
+
f"least ${max_spend}. If the CLI is not installed, run the setup commands above first."
|
|
116
|
+
),
|
|
117
|
+
"command": (
|
|
118
|
+
f"agentscore-pay pay POST {input.url} --chain base -H 'X-Operator-Token: {op_token}' "
|
|
119
|
+
f"-H 'Content-Type: application/json' -d '{input.retry_body_json}' --max-spend {max_spend}"
|
|
120
|
+
),
|
|
121
|
+
"what_it_does": (
|
|
122
|
+
"Hits this endpoint, receives this same 402, signs an EIP-3009 USDC TransferWithAuthorization "
|
|
123
|
+
"on Base, submits via X-Payment header. Server verifies + settles via the Coinbase facilitator + "
|
|
124
|
+
"returns 200 with the completed order."
|
|
125
|
+
),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if input.rails.x402_solana:
|
|
129
|
+
s = input.rails.x402_solana
|
|
130
|
+
block["x402_solana"] = {
|
|
131
|
+
"setup": PAY_SETUP_SOLANA,
|
|
132
|
+
"prerequisite": (
|
|
133
|
+
f"Run `agentscore-pay balance --chain solana` and confirm USDC balance on Solana ({s.network}) "
|
|
134
|
+
f"is at least ${max_spend}. If the CLI is not installed, run the setup commands above first."
|
|
135
|
+
),
|
|
136
|
+
"command": (
|
|
137
|
+
f"agentscore-pay pay POST {input.url} --chain solana -H 'X-Operator-Token: {op_token}' "
|
|
138
|
+
f"-H 'Content-Type: application/json' -d '{input.retry_body_json}' --max-spend {max_spend}"
|
|
139
|
+
),
|
|
140
|
+
"what_it_does": (
|
|
141
|
+
"Hits this endpoint, receives this same 402, signs an SPL Token TransferChecked transaction on "
|
|
142
|
+
"Solana, submits via X-Payment header. Server verifies + settles via the Coinbase facilitator + "
|
|
143
|
+
"returns 200 with the completed order."
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if input.rails.stripe:
|
|
148
|
+
cfg = input.rails.stripe
|
|
149
|
+
amount_cents = round(total_num * 100)
|
|
150
|
+
link_cli_blocked = amount_cents > 50000
|
|
151
|
+
product_name = cfg.product_name or "this purchase"
|
|
152
|
+
spt_context = (
|
|
153
|
+
f'Purchasing "{product_name}" via the agent commerce API. The user authorized this purchase '
|
|
154
|
+
f"through their AI agent for ${total_num}; charge to be settled via shared payment token over the "
|
|
155
|
+
"Machine Payments Protocol."
|
|
156
|
+
)
|
|
157
|
+
stripe_block: dict[str, Any] = {
|
|
158
|
+
"prerequisite": (
|
|
159
|
+
"Either your own Stripe account with Shared Payment Token acceptance, OR a Stripe Link wallet "
|
|
160
|
+
"(any user with link.com)."
|
|
161
|
+
),
|
|
162
|
+
"instructions": (
|
|
163
|
+
"Mint a SharedPaymentToken scoped to the profile_id advertised in accepted_methods, then submit "
|
|
164
|
+
"via Authorization: Payment MPP header with method=stripe/charge."
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
if cfg.profile_id and not link_cli_blocked:
|
|
168
|
+
stripe_block["setup_link_cli"] = [
|
|
169
|
+
"npm install -g @stripe/link-cli # or use npx -y @stripe/link-cli for one-shot",
|
|
170
|
+
"link-cli auth login # one-time, opens your Link wallet",
|
|
171
|
+
"link-cli payment-methods list --output-json # copy a csmrpd_... id",
|
|
172
|
+
]
|
|
173
|
+
stripe_block["command_link_cli"] = [
|
|
174
|
+
(
|
|
175
|
+
"SPEND_ID=$(link-cli spend-request create "
|
|
176
|
+
"--payment-method-id <csmrpd_id_from_payment_methods_list> "
|
|
177
|
+
f"--credential-type shared_payment_token --network-id {cfg.profile_id} "
|
|
178
|
+
f"--amount {amount_cents} "
|
|
179
|
+
f'--context "{spt_context}" --request-approval --output-json | jq -r .id)'
|
|
180
|
+
),
|
|
181
|
+
(
|
|
182
|
+
f"link-cli mpp pay {input.url} --spend-request-id $SPEND_ID --method POST "
|
|
183
|
+
f"--data '{input.retry_body_json}' --header 'X-Operator-Token: {op_token}' --output-json"
|
|
184
|
+
),
|
|
185
|
+
]
|
|
186
|
+
stripe_block["what_it_does_link_cli"] = (
|
|
187
|
+
"For users who have a Stripe Link wallet: step 1 mints a one-time-use SharedPaymentToken scoped to "
|
|
188
|
+
"this purchase and pushes a notification to the user for approval (blocks until approved); step 2 "
|
|
189
|
+
"submits the SPT via the MPP handshake along with your AgentScore operator credential."
|
|
190
|
+
)
|
|
191
|
+
elif link_cli_blocked:
|
|
192
|
+
stripe_block["note"] = (
|
|
193
|
+
"link-cli SPT path not available for this purchase — Stripe link-cli caps spend requests at $500.00 "
|
|
194
|
+
f"($50000 cents); your total is ${total_num}. Use your own Stripe account with the SharedPaymentToken "
|
|
195
|
+
"API instead."
|
|
196
|
+
)
|
|
197
|
+
block["stripe"] = stripe_block
|
|
198
|
+
|
|
199
|
+
return block
|