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
57 changes: 8 additions & 49 deletions agentscore_commerce/challenge/__init__.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,35 @@
"""402-body builders + pricing/receipt/agent-memory helpers."""

from agentscore_commerce.challenge.accepted_methods import (
BuildAcceptedMethodsInput,
SolanaMppConfig,
StripeConfig,
TempoConfig,
X402BaseConfig,
build_accepted_methods,
)
from agentscore_commerce.challenge.agent_instructions import (
BuildAgentInstructionsInput,
build_agent_instructions,
)
from agentscore_commerce.challenge.accepted_methods import build_accepted_methods
from agentscore_commerce.challenge.agent_instructions import build_agent_instructions, compatible_clients_by_rails
from agentscore_commerce.challenge.agent_memory import (
AgentMemoryHint,
build_agent_memory_hint,
first_encounter_agent_memory,
)
from agentscore_commerce.challenge.body import Build402BodyInput, X402PaymentRequired, build_402_body
from agentscore_commerce.challenge.how_to_pay import (
BuildHowToPayInput,
HowToPayRails,
SolanaMppRailConfig,
StripeRailConfig,
TempoRailConfig,
X402BaseRailConfig,
build_how_to_pay,
)
from agentscore_commerce.challenge.identity import (
IdentityMetadataInput,
IdentityMode,
SignerMatchResult,
build_identity_metadata,
)
from agentscore_commerce.challenge.body import X402PaymentRequired, build_402_body
from agentscore_commerce.challenge.how_to_pay import build_how_to_pay
from agentscore_commerce.challenge.identity import IdentityMode, SignerMatchResult, build_identity_metadata
from agentscore_commerce.challenge.order_receipt import (
OrderNextSteps,
OrderProductInfo,
OrderReceipt,
ShippingAddress,
)
from agentscore_commerce.challenge.pricing import PricingBlock, build_pricing_block
from agentscore_commerce.challenge.respond_402 import Respond402Input, Respond402Result, respond_402
from agentscore_commerce.challenge.validation_error import (
BuildValidationErrorInput,
build_validation_error,
)
from agentscore_commerce.challenge.respond_402 import Respond402Result, respond_402
from agentscore_commerce.challenge.validation_error import build_validation_error

__all__ = [
"AgentMemoryHint",
"Build402BodyInput",
"BuildAcceptedMethodsInput",
"BuildAgentInstructionsInput",
"BuildHowToPayInput",
"BuildValidationErrorInput",
"HowToPayRails",
"IdentityMetadataInput",
"IdentityMode",
"OrderNextSteps",
"OrderProductInfo",
"OrderReceipt",
"PricingBlock",
"Respond402Input",
"Respond402Result",
"ShippingAddress",
"SignerMatchResult",
"SolanaMppConfig",
"SolanaMppRailConfig",
"StripeConfig",
"StripeRailConfig",
"TempoConfig",
"TempoRailConfig",
"X402BaseConfig",
"X402BaseRailConfig",
"X402PaymentRequired",
"build_402_body",
"build_accepted_methods",
Expand All @@ -81,6 +39,7 @@
"build_identity_metadata",
"build_pricing_block",
"build_validation_error",
"compatible_clients_by_rails",
"first_encounter_agent_memory",
"respond_402",
]
127 changes: 61 additions & 66 deletions agentscore_commerce/challenge/accepted_methods.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,85 @@
"""accepted_methods[] builder for enriched 402 bodies."""

from dataclasses import dataclass, field
from typing import Any

_DEFAULT_TEMPO = {
"network": "tempo-mainnet",
"chain_id": 4217,
"token": "0x20C000000000000000000000b9537d11c60E8b50",
"symbol": "USDC.e",
"decimals": 6,
}
_DEFAULT_X402_BASE = {
"network": "eip155:8453",
"chain_id": 8453,
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
}
_DEFAULT_SOLANA_MPP = {
"network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"symbol": "USDC",
"decimals": 6,
}
_DEFAULT_STRIPE_RAILS = ["card", "link", "shared_payment_token"]

@dataclass
class TempoConfig:
recipient: str
network: str = "tempo-mainnet"
chain_id: int = 4217
token: str = "0x20C000000000000000000000b9537d11c60E8b50"
symbol: str = "USDC.e"
decimals: int = 6

def build_accepted_methods(
*,
tempo: dict[str, Any] | None = None,
x402_base: dict[str, Any] | None = None,
solana_mpp: dict[str, Any] | None = None,
stripe: dict[str, Any] | None = None,
) -> list[dict[str, Any]]:
"""Build the accepted_methods[] array. Each rail entry conditionally included if vendor passed it.

@dataclass
class X402BaseConfig:
recipient: str
network: str = "eip155:8453"
chain_id: int = 8453
token: str = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
symbol: str = "USDC"
decimals: int = 6


@dataclass
class SolanaMppConfig:
recipient: str
network: str = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
token: str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
symbol: str = "USDC"
decimals: int = 6


@dataclass
class StripeConfig:
profile_id: str | None = None
rails: list[str] = field(default_factory=lambda: ["card", "link", "shared_payment_token"])


@dataclass
class BuildAcceptedMethodsInput:
tempo: TempoConfig | None = None
x402_base: X402BaseConfig | None = None
solana_mpp: SolanaMppConfig | None = None
stripe: StripeConfig | None = None


def build_accepted_methods(input: BuildAcceptedMethodsInput) -> list[dict[str, Any]]:
"""Build the accepted_methods[] array. Each rail entry conditionally included if vendor passed it."""
Each rail value is a plain dict. Required key: ``recipient`` (or ``profile_id`` for stripe).
Optional keys override the rail's protocol defaults: ``network``, ``chain_id``, ``token``,
``symbol``, ``decimals`` (for chain rails) or ``rails`` (for stripe).
"""
out: list[dict[str, Any]] = []
if input.tempo:
if tempo:
out.append(
{
"method": "tempo/charge",
"network": input.tempo.network,
"chain_id": input.tempo.chain_id,
"token": input.tempo.token,
"symbol": input.tempo.symbol,
"decimals": input.tempo.decimals,
"pay_to": input.tempo.recipient,
"network": tempo.get("network", _DEFAULT_TEMPO["network"]),
"chain_id": tempo.get("chain_id", _DEFAULT_TEMPO["chain_id"]),
"token": tempo.get("token", _DEFAULT_TEMPO["token"]),
"symbol": tempo.get("symbol", _DEFAULT_TEMPO["symbol"]),
"decimals": tempo.get("decimals", _DEFAULT_TEMPO["decimals"]),
"pay_to": tempo["recipient"],
}
)
if input.x402_base:
if x402_base:
out.append(
{
"method": "x402/exact",
"network": input.x402_base.network,
"chain_id": input.x402_base.chain_id,
"token": input.x402_base.token,
"symbol": input.x402_base.symbol,
"decimals": input.x402_base.decimals,
"pay_to": input.x402_base.recipient,
"network": x402_base.get("network", _DEFAULT_X402_BASE["network"]),
"chain_id": x402_base.get("chain_id", _DEFAULT_X402_BASE["chain_id"]),
"token": x402_base.get("token", _DEFAULT_X402_BASE["token"]),
"symbol": x402_base.get("symbol", _DEFAULT_X402_BASE["symbol"]),
"decimals": x402_base.get("decimals", _DEFAULT_X402_BASE["decimals"]),
"pay_to": x402_base["recipient"],
}
)
if input.solana_mpp:
if solana_mpp:
out.append(
{
"method": "x402/exact",
"network": input.solana_mpp.network,
"token": input.solana_mpp.token,
"symbol": input.solana_mpp.symbol,
"decimals": input.solana_mpp.decimals,
"pay_to": input.solana_mpp.recipient,
"network": solana_mpp.get("network", _DEFAULT_SOLANA_MPP["network"]),
"token": solana_mpp.get("token", _DEFAULT_SOLANA_MPP["token"]),
"symbol": solana_mpp.get("symbol", _DEFAULT_SOLANA_MPP["symbol"]),
"decimals": solana_mpp.get("decimals", _DEFAULT_SOLANA_MPP["decimals"]),
"pay_to": solana_mpp["recipient"],
}
)
if stripe:
out.append(
{
"method": "stripe/charge",
"rails": stripe.get("rails", _DEFAULT_STRIPE_RAILS),
"profile_id": stripe.get("profile_id"),
}
)
if input.stripe:
out.append({"method": "stripe/charge", "rails": input.stripe.rails, "profile_id": input.stripe.profile_id})
return out
66 changes: 27 additions & 39 deletions agentscore_commerce/challenge/agent_instructions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""agent_instructions block builder for the 402 body."""

from collections.abc import Iterable
from dataclasses import dataclass, field
from typing import Any, Literal

_TEMPO_WARNING = (
Expand Down Expand Up @@ -74,7 +73,7 @@ def compatible_clients_by_rails(rails: Iterable[str]) -> dict[str, list[str]] |
def _default_compatible_clients(how_to_pay: dict[str, Any]) -> dict[str, list[str]] | None:
"""Default ``compatible_clients`` derived from the rails declared in ``how_to_pay``.

Vendors override this in ``BuildAgentInstructionsInput(compatible_clients=...)``
Vendors override via the ``compatible_clients`` kwarg of ``build_agent_instructions``
to add their own tested clients or remove entries that don't fit their endpoint.
Verified state as of the SDK release.
"""
Expand All @@ -90,55 +89,44 @@ def _default_compatible_clients(how_to_pay: dict[str, Any]) -> dict[str, list[st
return compatible_clients_by_rails(rails)


@dataclass
class BuildAgentInstructionsInput:
how_to_pay: dict[str, Any]
recommended_tools: list[str] | None = None
wallet_compatibility: str | None = None
timeout_seconds: int = 300
warnings: list[str] | None = None
def build_agent_instructions(
*,
how_to_pay: dict[str, Any],
recommended_tools: list[str] | None = None,
wallet_compatibility: str | None = None,
timeout_seconds: int = 300,
warnings: list[str] | None = None,
# Appended to the default protocol-footgun warnings. Use this to keep the SDK's
# protocol warnings AND add merchant-specific notes. Ignored when ``warnings`` is set.
extra_warnings: list[str] | None = None
recommended: str | None = None
extra_warnings: list[str] | None = None,
recommended: str | None = None,
# Per-rail list of client names the merchant has verified work end-to-end.
# Vendors set this from their own smoke matrix — defaults to None, in which case
# the field is not emitted (avoids vouching for clients the merchant has not tested).
# Keys are rail identifiers (e.g. "x402_base", "tempo_mpp"); values are display labels.
compatible_clients: dict[str, list[str]] | None = None
extra: dict[str, Any] = field(default_factory=dict)


def build_agent_instructions(input: BuildAgentInstructionsInput) -> dict[str, Any]:
compatible_clients: dict[str, list[str]] | None = None,
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the agent_instructions block — combines how_to_pay with tools, warnings, compat note, timeout.

Defaults adapt to the rails declared in ``how_to_pay``: only tempo-relevant warnings/tools
appear if ``how_to_pay["tempo"]`` is set, only x402-relevant ones if ``x402_base``/
``solana_mpp`` are set. Vendors override ``warnings``/``recommended_tools`` for full control.
"""
recommended_tools = (
input.recommended_tools if input.recommended_tools is not None else _default_recommended_tools(input.how_to_pay)
)
warnings = (
input.warnings
if input.warnings is not None
else [*_default_warnings(input.how_to_pay), *(input.extra_warnings or [])]
)
compatible_clients = (
input.compatible_clients
if input.compatible_clients is not None
else _default_compatible_clients(input.how_to_pay)
)
resolved_tools = recommended_tools if recommended_tools is not None else _default_recommended_tools(how_to_pay)
resolved_warnings = warnings if warnings is not None else [*_default_warnings(how_to_pay), *(extra_warnings or [])]
resolved_clients = compatible_clients if compatible_clients is not None else _default_compatible_clients(how_to_pay)
out: dict[str, Any] = {
"how_to_pay": input.how_to_pay,
"recommended_tools": recommended_tools,
"wallet_compatibility": input.wallet_compatibility or DEFAULT_WALLET_COMPATIBILITY,
"timeout_seconds": input.timeout_seconds,
"warnings": warnings,
"how_to_pay": how_to_pay,
"recommended_tools": resolved_tools,
"wallet_compatibility": wallet_compatibility or DEFAULT_WALLET_COMPATIBILITY,
"timeout_seconds": timeout_seconds,
"warnings": resolved_warnings,
}
if input.recommended:
out["recommended"] = input.recommended
if compatible_clients:
out["compatible_clients"] = compatible_clients
out.update(input.extra)
if recommended:
out["recommended"] = recommended
if resolved_clients:
out["compatible_clients"] = resolved_clients
if extra:
out.update(extra)
return out
4 changes: 2 additions & 2 deletions agentscore_commerce/challenge/agent_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ def first_encounter_agent_memory(

Use directly with the ``agent_memory`` field of :func:`build_402_body`::

body = build_402_body(Build402BodyInput(
body = build_402_body(
accepted_methods=accepted,
agent_instructions=instructions,
pricing=pricing,
agent_memory=first_encounter_agent_memory(
first_encounter=not has_seen_operator(operator_token),
),
))
)

Returning ``None`` means ``build_402_body`` cleanly skips the field instead of
emitting ``agent_memory: null`` (which would imply "I tried but failed" rather than
Expand Down
Loading