Multi-token settlement + agent wallet + MCP/CLI + Hanzo.ai compat + escrow thinking-chains#135
Draft
Pattermesh wants to merge 44 commits into
Draft
Multi-token settlement + agent wallet + MCP/CLI + Hanzo.ai compat + escrow thinking-chains#135Pattermesh wants to merge 44 commits into
Pattermesh wants to merge 44 commits into
Conversation
Two-layer design: multi-token settlement standard (generalizes the native-ETH A2A escrow EIP) + built-in agent wallet (session-key delegation + load-balancing router). Contract-generalization strategy left as an open decision for @abhicris. Decomposed into 14 parallel units for a contribution wave.
…ss policy, escrow metrics; delivery boundaries (review-first, contract=draft-PR-for-abhicris, Solana separate track)
…s (TDD, per-unit files/interfaces/tests)
Multi-token A2A escrow interface generalizing the native-ETH EIP: - Payment struct carries an `address token` field (address(0) = ETH profile) - Declares full lifecycle: createPayment / confirmPayment / releaseByAttestation / requestRefund / cancelPayment / getPayment - Every event carries `token` for per-asset indexing - TDD: contracts/test/IAgentEscrow.t.sol proves a stub `is IAgentEscrow` compiles and the Payment struct exposes `.token` Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add SettlementToken dataclass and negotiate_settlement_token() to src/payment_protocol.py. Negotiation is deterministic: intersect accepted sets by (chain_id, token), sum payer_rank + payee_rank, return highest combined rank; tie-break by lexicographic token address. PaymentRequest gains settlement_token (Optional[SettlementToken] = None) excluded from content_hash() so pre-negotiation and post-negotiation requests hash identically. currency retained as v1.1-compatible alias. to_json() omits settlement_token when None so v1.0/v1.1 wire bytes are byte-for-byte unchanged (protocol_vectors conformance suite still green). Tests: tests/test_payment_protocol_negotiation.py (18 cases green). Docs: bumps agent-payment-protocol.md to v1.2 in follow-up commit.
Add settlement_token field to §2 table; new §2.3 documents the SettlementToken structure and negotiate_settlement_token() algorithm (highest combined rank, deterministic tiebreak by token address). content_hash exclusion rules updated to include settlement_token. §12 version notes list v1.2 as a non-breaking extension.
… (Unit ⑧) - switchboard/treasury.py: thread-safe per-(chain_id,token) balance store with credit/debit/spendable/reserves; InsufficientBalance on overdraft; featured partner tokens LUX/ZOO are first-class. - switchboard/agent_wallet.py: AgentWallet(mpc,treasury,escrow) wrapping MPCWallet; pay(request)->receipt entrypoint validates, debits treasury, MPC-signs, and drives the EscrowClient seam; EscrowClient is a @runtime_checkable Protocol — the real on-chain client wires in later. - tests/test_treasury.py + tests/test_agent_wallet.py: 30 tests, all green. EscrowClient seam: tests use MagicMock(spec=EscrowClient); real MultiTokenAgentEscrow client connects when Unit ① lands.
- switchboard/delegation.py: Delegation class with grant(agent_id, policy) -> SessionKey and revoke(key); SpendPolicy(token_allowlist, per_tx_cap, daily_cap, expires_at, allowed_counterparties); fail-fast enforcement order: revoked → expired → token allowlist → counterparty → per_tx_cap → daily_cap (rolling 24h via GasManager) → delegates to AgentWallet.pay(). Module-level grant()/revoke() helpers use a process-wide default Delegation. - tests/test_delegation.py: 21 tests covering all enforcement paths — cap, expiry, allowlist, counterparty, revocation, unlimited (None) cases. - Reuses switchboard/gas_manager.GasManager for daily rolling-window spend tracking; per_tx_cap enforced as direct comparison (typed error message). DRY: no gas-budgeting or nonce logic reinvented.
switchboard/metrics.py
- Defines three input record types (EscrowEvent, WalletOpEvent,
EscrowState) that are the canonical contract for what the backend
must emit.
- compute_escrow_metrics(): fill rate, timeout rate, refund rate,
challenge rate, avg time-to-release, pending count from state
snapshots. Rates are over terminal events only (Locked/pending
events excluded from denominators).
- compute_wallet_ops_metrics(): spend by token/rail (denied ops
excluded), policy denial count + breakdown by reason.
- compute_fleet_health(): per-wallet op counts, active wallet count,
denial rate per wallet.
- compute_all_metrics(): aggregates all three into AllMetrics bundle.
tests/test_metrics.py (35 tests, all green)
- Full TDD: tests written first, watched fail, minimal implementation
written to pass. Tests cover record shape validation, all five
escrow metrics, edge cases (empty data → None rates, Locked events
not counted in denominators), wallet ops (multi-token, denied ops
excluded), fleet health, and a full round-trip fixture.
web/metrics.html
- Dashboard panel matching web/ conventions (JetBrains Mono,
kcolbchain brand tokens, dark theme, .stat/.card/.chip patterns).
- Renders: fill-rate ring, 5 escrow stats, avg time-to-release,
pending count, sparkline (hourly fill rate, 24 h), spend-by-token
and spend-by-rail bar charts, policy-denial breakdown, fleet table
with per-wallet denial rate + health chip.
- fetchMetrics() wired to mock data with jitter; replace with
/api/metrics for production. Poll interval configurable (10/30/60 s)
with live indicator dot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New sibling contract (AgentEscrow.sol untouched) implementing IAgentEscrow for both native ETH and ERC-20 settlement: - token == address(0): native ETH via msg.value (parity with AgentEscrow; msg.value must equal amount; low-level call payout) - token != address(0): ERC-20 pulled via SafeERC20.transferFrom on create, paid out via SafeERC20.transfer on release/refund/cancel - Balance-delta accounting: credited amount = measured balance increase across transferFrom, so fee-on-transfer / rebasing tokens are safe (escrow never releases more than it holds) - Per-token allowlist gates ERC-20s (setTokenAllowed, onlyOwner); native ETH is always allowed as the core profile - Every event carries `token` - nonReentrant + checks-effects-interactions on all transfer paths - Oracle release (createPaymentWithPolicy / releaseByAttestation) preserved Mocks: MockERC20 (standard), MockFeeOnTransferERC20 (1% burn-on-transfer). Tests (26): ETH parity, ERC-20 happy path, fee-on-transfer balance-delta (credit 990 for declared 1000; no underflow on payout), timeout/refund/ challenge/cancel, non-allowlisted rejected, lifecycle guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add AcceptedToken dataclass to switchboard/x402/server.py with
{chain_id, token, min_amount, rank} fields and to_dict/from_dict.
PaymentRequirements gains an accepts: List[AcceptedToken] field;
to_header() includes it when non-empty; from_header()/from_dict()
deserialise it back. Back-compat: absent accepts[] is ignored.
X402Server gains:
- accepts kwarg (list of AcceptedToken; [] = no restriction)
- validate_settlement_token(chain_id, token) -> (bool, str)
- build_402_response() now embeds accepts[] in X-Payment-Required header
PaymentOffer gains token: str | None = None (v1.2); from_header()
reads it from the JSON "token" key (absent in v1.1 payloads → None).
X402Middleware gains:
- accepted_tokens kwarg (None = back-compat no-op; [] = reject all)
- _validate_settlement_token(chain_id, token) — raises on mismatch
- _validate_offer() now calls _validate_settlement_token when offer.token is set
Tests: tests/test_x402_multitoken.py (23 cases green).
Adds web/onboarding.html — a single-page, SPA-style onboarding flow
for the agent wallet (spec §10, plan unit ⑱). Four views in one file
with hash-based routing, no build step required:
• Login / sign-in — email magic-link + API-key auth modes;
animated payment-flow canvas on the left panel.
• Connect API key — provider tabs (Anthropic / OpenAI / Google /
Cohere / Custom), masked-key display, add/delete, encrypted-at-rest
notice, live saved-keys list.
• Connect agent — MCP WebSocket endpoint + session key display,
copy button, spend policy grid (per-tx cap, daily cap, token
allowlist, TTL), full MCP tool catalogue with Python code sample.
• Operate the wallet in 3 steps — numbered walkthrough
(token negotiation → rail selection + MPC signing → on-chain
settlement), code samples for each step, security model cards.
• Metrics dashboard — treasury balances per (chain, token),
spend by token/rail bar charts, escrow health table, fleet
health bar; auto-polls every 30 s via MockAPI.
Adds web/mock-api.js — documented endpoint contract + in-browser
stub. Backend team wires real endpoints to match the contract:
POST/DELETE /api/auth/session
POST/GET/DELETE /api/keys
GET /api/agent/mcp-endpoint
GET /api/wallet/balances
POST /api/escrow/create
GET /api/policy/status
GET /api/metrics
Updates web/index.html nav and web/lab/_build.js sidebar to link
the new page. Matches the existing "Luxury Observatory" design
language: Obsidian + gold, Cormorant Garamond + DM Sans +
JetBrains Mono, shared CSS conventions from web/lab/shared.css.
No build step — open web/onboarding.html directly in a browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… token-bucket, compliance
Implements Unit ⑲ from the agent-wallet multitoken-settlement plan:
* `switchboard/access_policy.py`
- `AgentTier` enum: EXPLORER / STANDARD / TRUSTED with distinct per-tx caps.
- `TokenBucketConfig` + `TierConfig`: injectable bucket params (rate, capacity,
per_tx_cap) with sensible defaults (explorer 1k/10tok, standard 10k/50tok,
trusted 100k/200tok).
- `AccessPolicy.check(agent_id, action) -> Decision`: 4-layer evaluation —
Layer 1 contract-compliance (zero/negative amounts, terminal escrow states)
Layer 2 SpendPolicy (token allowlist, expiry, per-tx cap, counterparties)
Layer 3 tier ceiling (per-agent per-tx cap from TierConfig)
Layer 4 token-bucket rate fairness (per-agent, independent, refills at rate/s)
- `Decision(agent_id, allowed, reason, event)` — typed, immutable.
- `WalletOpEvent(denied, denial_reason, agent_id)` — metric payload on every call;
optional `event_listener` callback for the ⑳ dashboard / metrics backend.
- Denial reasons: "noncompliant" | "policy_violation" | "tier_ceiling" | "rate_limited"
- Module-level `check()` convenience (MCP server + Router call site).
- Thread-safe: single reentrant lock; injectable clock for deterministic tests.
* `tests/test_access_policy.py` (28 tests, all green)
- TestBasicAllow: allow path, agent_id echo, unknown-agent explorer fallback,
module-level helper.
- TestTierCeilings: explorer/standard/trusted ceilings enforced; tier upgrade.
- TestRateFairness: bucket exhaustion → rate_limited; N agents each bounded to
capacity (rate=0 quota mode); refill over time; 3-thread concurrent safety.
- TestContractCompliance: terminal escrow states refused; zero/negative amounts;
open/confirmed states allowed.
- TestSpendPolicyIntegration: token allowlist, expiry, per-tx cap from SpendPolicy.
- TestWalletOpEvents: denied/allowed events; event_listener callback.
- TestDecision: field validation; typed reason strings.
Also staged: Batch-1 deps brought in from pattermesh/agent-wallet-multitoken-settlement
(delegation.py, gas_manager.py, gas_budget.py, agent_wallet.py, treasury.py, docs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds two docs-only deliverables for unit ④ of the agent-wallet-multitoken-settlement design: - eips/draft-multitoken-a2a-escrow.md — Standards-Track ERC draft that generalizes the native-ETH A2A escrow (draft-native-eth-a2a-escrow.md) to any settlement asset. The ETH profile (token == address(0)) is a strict subset; the existing native-ETH ERC becomes a named profile of this standard. Covers: IAgentEscrow interface (matching contracts/IAgentEscrow.sol), ERC-165 id 0x01dc5a49, ERC-20 + native-ETH semantics, fee-on-transfer via balance-delta accounting, per-token allowlist, oracle-release opt-in via policyHash, and full Security Considerations section. - eips/magicians-post-multitoken.md — ethereum-magicians forum post template announcing the draft, with six open questions for community review. No tests, no contract changes (contracts/IAgentEscrow.sol and contracts/MultiTokenAgentEscrow.sol brought in from pattermesh/agent-wallet-multitoken-settlement as references only). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…page Implements Unit ⑩ of the agent-wallet-multitoken-settlement spec. - `TokenCandidate(token, fee_bps=0, expected_slippage_bps=0)` dataclass - `TokenSelector(treasury, chain_id).select(amount, candidates) -> TokenCandidate|None` filters by spendable balance then sorts by (fee_bps, slippage_bps, token) - LUX/ZOO partner tokens work as first-class candidates (no special-casing) - 11 pytest tests covering solvency, fee preference, slippage tie-break, partner tokens, empty candidates Co-Authored-By: Pattermesh <pattermesh@gmail.com>
Implements Unit ⑪ of the agent-wallet-multitoken-settlement spec. - `RailConfig(x402_max_amount, escrow_max_amount)` with sensible defaults - `RailSelector(config).select(amount, force_rail=None) -> str` returns "x402" for micro, "escrow" for mid-range, "mpp" for large - `force_rail` allows caller override (policy enforcement is upstream) - 10 pytest tests covering all three thresholds + force override + defaults Co-Authored-By: Pattermesh <pattermesh@gmail.com>
…nceManager Implements Unit ⑫ of the agent-wallet-multitoken-settlement spec. - `FleetBalancer(wallets, nonce_manager, chain_id).pick(chain_id) -> str` selects the wallet with fewest pending nonces (least-loaded) - Thread-safe via internal lock — concurrent picks rotate correctly - Raises ValueError on empty fleet - Reuses existing `NonceManager` for nonce tracking (no reinvention) - 8 pytest tests: single wallet, least-busy routing, distribution over 9 picks, concurrent-thread safety, empty fleet error Co-Authored-By: Pattermesh <pattermesh@gmail.com>
…cation Implements Unit ⑬ of the agent-wallet-multitoken-settlement spec. - `RebalanceTarget(token, target_pct)` and `SwapIntent(from_token, to_token, amount, chain_id)` frozen dataclasses - `Rebalancer(treasury, chain_id, min_rebalance_pct=1.0).rebalance_targets(targets) -> List[SwapIntent]` — emits intended swaps; does NOT execute them - Validates targets sum to 100%; skips swaps below min_rebalance_pct threshold - Multi-underweight pairing: emits one intent per underweight token - LUX/ZOO work as first-class allocation targets - 10 pytest tests: balanced=no-op, overweight sell, threshold filtering, partner tokens (LUX+ZOO), invalid targets error, dataclass shapes Co-Authored-By: Pattermesh <pattermesh@gmail.com>
…pEvent
Top-level Router composing the four pluggable strategies into a single
call surface (spec §4.3/§4.4).
- `Plan(token, rail, wallet)` frozen dataclass — the routing decision
- `Router(token_selector, rail_selector, fleet_balancer, events=None)`
- `Router.route(chain_id, amount, candidates, agent_id, force_rail) -> Plan`
- Calls TokenSelector → RailSelector → FleetBalancer in sequence
- Emits a `WalletOpEvent` (metrics.py contract) after every route,
including denied events when no solvent token is found
- Raises ValueError with "No solvent token" on exhausted candidates
- `__init__.py` exports: Router, Plan, TokenSelector, TokenCandidate,
RailSelector, FleetBalancer, Rebalancer, RebalanceTarget, SwapIntent
- 13 pytest tests: Plan shape, rail selection with explicit RailConfig,
WalletOpEvent emission (allowed + denied), no-solvent-token error
Total: 43 new tests, 266 passing suite-wide (0 failures).
Co-Authored-By: Pattermesh <pattermesh@gmail.com>
Shared object store for the agent-wallet-multitoken-settlement wave: IAgentEscrow interface + MultiTokenAgentEscrow (approach A sibling) + standard/fee-on-transfer ERC-20 mocks + their Foundry suites + spec/plan. Base for the swap adapter (unit ②) built next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in layer (spec §3.5) that converts the escrowed payer-token into the payee's desired token AT RELEASE, kept OUTSIDE the escrow core so the trustless primitive has zero DEX/oracle attack surface. - settleWithSwap(requestId, tokenOut, maxSlippageBps): confirms the escrow (adapter is the escrow payer/payee for swap-settled payments), receives tokenIn, prices the swap via IPriceOracle, swaps via ISwapRouter, and forwards tokenOut to the real payee. - Reverts the WHOLE release (escrow stays funded + Locked) when realized slippage > maxSlippageBps OR the oracle price is stale. - Minimal ISwapRouter + IPriceOracle interfaces (real lucidly wiring later) with MockSwapRouter / MockPriceOracle for TDD. - Adapter independently re-checks realized output (never trusts the router's own floor alone); nonReentrant across the escrow↔adapter↔router↔payee chain. Tests (8): successful X→Y swap; slippage revert via the adapter's own guard (broken router) AND via the router floor — both leave escrow funded; oracle- stale rejection; auth + tokenOut/slippage-tamper guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nit ③) - Pin forge-std to v1.9.4 in CI so it matches the version the contracts are developed/tested against (was unpinned --shallow, could drift). - Document that forge auto-discovers contracts/test + test/ so unit ① and ② suites already run under the existing forge build/test steps. - Add an explicit assert step running MultiTokenAgentEscrowTest and SwapSettlementAdapterTest so lost discovery (e.g. a bad path move) surfaces loudly instead of silently shrinking coverage. - Verified: OZ CI-pinned SHA dbb6104 == the v5.0.2 release tag (same code we test against locally). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add switchboard/tools.py with TOOL_DEFINITIONS for all 7 agent-facing tools (wallet_balance, pay, create_escrow, confirm_payment, request_refund, policy_status, escrow_metrics). Each ToolDef carries name, description, JSON-Schema, op, and access-policy metadata. Define the AccessPolicy / Decision / AllowAllPolicy seam that Unit ⑲ will wire at integration time. Extend registry.json with a "tools" key via sync_registry_json(). Tests in tests/test_tools_registry.py (46 tests). Co-Authored-By: Pattermesh <pattermesh@gmail.com>
Add switchboard/mcp_server.py: JSON-RPC 2.0 over stdio. Tools: wallet_balance, pay, create_escrow, confirm_payment, request_refund, policy_status, escrow_metrics — each gated by session key + access policy. Reads tool definitions from the ⑰ registry (DRY). Wires the AccessPolicy seam (AllowAllPolicy stub by default; real Unit ⑲ impl plugs in via access_policy= constructor arg). Tests in tests/test_mcp_server.py (52 tests). Co-Authored-By: Pattermesh <pattermesh@gmail.com>
Add switchboard/cli.py: click-based CLI exposing wallet balance|grant|revoke, escrow create|confirm|refund|status, metrics, tools list, and mcp-server. Register console-script 'switchboard = switchboard.cli:main' in pyproject.toml. CLI reads tool definitions from the ⑰ registry (DRY with MCP). All commands produce JSON output. Tests in tests/test_cli.py (39 tests). Co-Authored-By: Pattermesh <pattermesh@gmail.com>
AgentWallet defined its own PaymentRequest; src/payment_protocol.py has the canonical one (v1.2, with settlement_token). Make agent_wallet import and re-export the canonical type so the wallet, delegation, MCP, and on-chain negotiation all speak one request shape. - src/payment_protocol.PaymentRequest gains a multi-token `token` field and an `amount` read/write property aliasing `amount_wei`. Both are kept OFF the frozen v1.0 wire (token omitted at default; amount is a property, not a field) and OUT of content_hash, so protocol vectors are byte-identical. - switchboard/agent_wallet.py drops its private PaymentRequest, imports the canonical one; behavior preserved. - Construction sites (mcp_server, tests) pass amount_wei= (canonical field). - New tests/test_integration_seams.py::TestSeam1CanonicalPaymentRequest. pytest: 564 passed / 62 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
access_policy defined its own minimal WalletOpEvent{denied,denial_reason,
agent_id}; the Router and the metrics dashboard already speak the canonical
metrics.WalletOpEvent. Replace the local type with an import of
metrics.WalletOpEvent so policy denials flow straight into the dashboard's
denial-rate / denials-by-reason / spend panels.
- access_policy._deny/_allow now build the full event (op_type, token, amount
from the action dict; rail/wallet_id empty at the policy layer; timestamp).
- _event() also tolerates the MCP Protocol form where action is an op-name
string (used by seam 3).
- Tests updated to the canonical event shape; new integration tests prove the
re-export identity and that emitted denials feed compute_wallet_ops_metrics.
pytest: 567 passed / 62 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MCP (tools.py / mcp_server.py) expects AccessPolicy.check(agent_id, action)->
Decision with .denied/.reason, where action is the tool op-name string. The
real engine (access_policy.AccessPolicy) took an action dict and returned a
Decision with .allowed/.reason. Reconcile both:
- access_policy.Decision gains a .denied property (= not allowed), so ONE
object satisfies the native (.allowed) and MCP (.denied) views.
- check() accepts the op-name string form and normalises it to {"type": op}
with no amount; the amount/token/payee-dependent layers only fire when those
keys are present, so the op-name gate enforces registration + expiry + tier
rate-fairness and defers full request validation to Delegation.pay_with_key.
Dict-form behavior (zero-amount noncompliant, token allowlist, per-tx cap) is
unchanged.
- mcp_server.main() now wires the REAL AccessPolicy() instead of AllowAllPolicy
(the __init__ default stays permissive for standalone/test use).
- New integration tests prove: Decision.denied/.reason present, op-name form
does not false-deny, and a denied op is refused THROUGH MCP (_POLICY_DENIED)
while a compliant agent's pay executes.
pytest: 571 passed / 62 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AgentWallet.pay() left a documented seam where the Router (units 10-12) and the
access-policy engine (unit 19) were meant to plug in. Wire them:
- AgentWallet(__init__) accepts optional router= and access_policy=.
- pay(request, agent_id="", candidates=None):
1. access_policy.check(agent_id, {type:pay,...}) BEFORE signing; a denied
decision raises AccessDenied(reason) with nothing debited/signed.
2. Router.route(...) selects (token, rail, wallet) and emits its canonical
WalletOpEvent; the selected token drives the debit/sign/escrow path.
3. PaymentReceipt now carries rail + wallet (None on the direct, no-Router
path, so existing behavior/tests are unchanged).
- Delegation.pay_with_key forwards key.agent_id into pay() so events/checks are
attributed to the acting agent.
- Synchronous create->release (vs waiting the on-chain challenge period) is left
as-is and documented in pay() as the intended wallet-side happy path.
- New integration tests: routed pay records rail/wallet + emits one event; a
denied engine blocks before MPC signs (nothing debited); allowed engine
proceeds; no-Router keeps the direct path.
pytest: 575 passed / 62 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add supportsInterface(0x01dc5a49) so off-chain clients (the Python EscrowClient, the swap adapter, explorers) can discover that the contract speaks the multi-token A2A escrow interface without a trial call. - MultiTokenAgentEscrow.supportsInterface returns true for type(IAgentEscrow).interfaceId (== 0x01dc5a49, the XOR of the six external selectors) and type(IERC165).interfaceId (0x01ffc9a7); false otherwise. - Imports OZ IERC165 (already vendored in lib/). - Foundry test_supportsInterface pins the id to 0x01dc5a49 and asserts the contract advertises IAgentEscrow + ERC-165 and rejects 0xffffffff / unrelated. forge test: 67 passed (was 66). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds switchboard/adapters/hanzo.py:
- normalize_402_body(): fixes the structural mismatch where switchboard's
X402Server puts payment details under payment_requirements{} but the
Hanzo fetch tool (hanzoai/mcp#fetch.ts) parses body.accepts[] top-level
per the x402.org v2 spec. Promotes accepts[] to the top level so Hanzo
agents can discover payment requirements without hand-rolling translation.
- build_hanzo_402_body(): builds a 402 body that is simultaneously
Hanzo-native (top-level accepts[]) and switchboard back-compat.
- encode/decode_hanzo_payment_header(): handles base64(JSON) encoding of
the X-PAYMENT header that Hanzo's fetch tool sends on retry.
- read_payment_header(): priority-ordered header reader (X-PAYMENT >
X-Payment > X-Payment-Proof) for unified client-side header handling.
- payment_requirements_from_hanzo_accepts(): converts Hanzo accepts[] to
switchboard PaymentRequirements + AcceptedToken multi-token list.
- HanzoAgentWallet: binds a Hanzo IAM identity ("admin/name" format) to a
switchboard AgentWallet + scoped SessionKey issued via Delegation.grant(),
so Hanzo agents operate their own wallets gated by SpendPolicy / AccessPolicy.
Adds tests/test_hanzo_adapter.py (48 tests, all green):
- Hanzo fetch tool inputSchema matches (X-PAYMENT encoding round-trip).
- Structural mismatch test: raw switchboard 402 body lacks top-level accepts;
after normalize_402_body() Hanzo can parse it.
- Multi-token accepts promotion.
- HanzoAgentWallet: connect → session key → pay → escrow within policy.
- SpendPolicy gates: per_tx_cap, daily_cap, token_allowlist, revocation.
- AccessPolicy denial/allow paths.
- End-to-end envelope-to-payment round-trip.
Adds docs/hanzo-compatibility.md with concrete wire format, call path
diagram, and caveats (stub escrow, router not wired by default).
Incompatibility found and fixed: switchboard 402 body uses
payment_requirements.{} while Hanzo fetch tool expects body.accepts[].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… InMemoryEscrowClient + multi-token demo
| const GOLD = '#d4a853'; | ||
| const OK = '#4ecb71'; | ||
| const FAINT = '#2a2430'; | ||
| const BG = '#06060b'; |
| payee: str, | ||
| ) -> str: | ||
| """Create an escrow entry; return an opaque escrow_id.""" | ||
| ... |
|
|
||
| def release_payment(self, escrow_id: str) -> bool: | ||
| """Release the escrowed funds to the payee; return True on success.""" | ||
| ... |
| def test_balance_single_token(self): | ||
| wallet = _patched_wallet(balance=42_000_000, token=USDC) | ||
| delegation = Delegation(wallet=wallet) | ||
| import switchboard.cli as cli_mod |
| def test_balance_all_tokens(self): | ||
| wallet = _patched_wallet(balance=100_000, token=USDC) | ||
| delegation = Delegation(wallet=wallet) | ||
| import switchboard.cli as cli_mod |
|
|
||
| def test_balance_output_is_json(self): | ||
| wallet = _patched_wallet(balance=1_000, token=USDC) | ||
| import switchboard.cli as cli_mod |
|
|
||
| class TestWalletGrant: | ||
| def _grant(self, *extra_args): | ||
| import switchboard.cli as cli_mod |
|
|
||
| class TestWalletRevoke: | ||
| def test_revoke_valid_key(self): | ||
| import switchboard.cli as cli_mod |
| cli_mod._delegation = None | ||
|
|
||
| def test_revoke_unknown_key_exits_nonzero(self): | ||
| import switchboard.cli as cli_mod |
|
|
||
| class TestEscrowCreate: | ||
| def _setup(self): | ||
| import switchboard.cli as cli_mod |
| return key.key_id | ||
|
|
||
| def _teardown(self): | ||
| import switchboard.cli as cli_mod |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Takes switchboard from native-ETH-only escrow to a full multi-token settlement standard + built-in agent wallet + agent-facing surfaces, plus Hanzo.ai compatibility and an escrow "thinking-chain" primitive for intelligent financial systems.
Design spec:
docs/agent-wallet-multitoken-settlement.md· Plan:docs/superpowers/plans/2026-07-01-agent-wallet-multitoken-settlement-plan.mdOpened as a draft — the §3.3 contract-generalization decision is @abhicris's call before this is marked ready (see below).
What's included
Settlement standard (Solidity) —
IAgentEscrow,MultiTokenAgentEscrow(approach A, sibling;AgentEscrow.soluntouched; ETH profile =token==address(0), ERC-20 viatransferFrom, fee-on-transfer via balance-delta, per-token allowlist, ERC-165),SwapSettlementAdapter(opt-in cross-token at release, oracle-bounded slippage, kept outside the escrow core), + the Multi-Token A2A Escrow ERC draft generalizing the native-ETH EIP.Protocol — settlement-token negotiation (
payment_protocolv1.2) + x402accepts[]multi-token envelope.Agent wallet (Python) —
AgentWallet/Treasury, session-keySpendPolicydelegation, a load-balancingRouter(token / rail / fleet / rebalance), a fairness + agent-access-policy engine (per-agent token-bucket, tiers, contract-compliance), and escrow-fulfilmentmetrics.Surfaces — MCP server (
switchboard mcp-server, the connect-your-agent endpoint), CLI, shared tool registry, and a full onboarding frontend (login → connect API key → connect agent → operate-wallet-in-3-steps) + live metrics dashboard.Hanzo.ai + thinking chains —
adapters/hanzo.py(Hanzo MCPfetchinterop +HanzoAgentWallet),thinking_chain.py(ThinkingChain+HanzoEscrowThinkingChain), a watchable multi-token demo, anddocs/thinking-chains-and-switchboard.md.Verification
python3 examples/multitoken_thinking_chain_demo.pyruns end-to-end (shows a chain halting correctly on a policy denial before any funds move).For @abhicris — decisions to make (review agenda)
supportsInterface(0x01dc5a49)added; EIP still needsdiscussions-toURL + handle verification beforeethereum/EIPs.pay()is synchronous single-step (Router wired in) — confirm whether the wallet happy-path should honor the on-chain challenge window.daily_capper session-key vs per-token;set_reserveis a spendable floor, not a hard debit lock.payment_requirements; x402.org v2 / Hanzo expect top-levelaccepts[](handled adapter-side now — consider emitting top-level natively).Hanzo.ai & acknowledgments
Hanzo agents can connect over MCP and operate policy-gated switchboard wallets; see
docs/hanzo-compatibility.md. Cross-org access (pattermesh / lux / hanzo / zoo) was provided by @zeekay — thank you. LUX and ZOO are featured as first-class partner tokens throughout.Preview
web/onboarding.htmlweb/metrics.htmlpython3 examples/multitoken_thinking_chain_demo.py🤖 Generated with Claude Code