Skip to content

Multi-token settlement + agent wallet + MCP/CLI + Hanzo.ai compat + escrow thinking-chains#135

Draft
Pattermesh wants to merge 44 commits into
mainfrom
pattermesh/agent-wallet-multitoken-settlement
Draft

Multi-token settlement + agent wallet + MCP/CLI + Hanzo.ai compat + escrow thinking-chains#135
Pattermesh wants to merge 44 commits into
mainfrom
pattermesh/agent-wallet-multitoken-settlement

Conversation

@Pattermesh

Copy link
Copy Markdown
Collaborator

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.md

Opened 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.sol untouched; ETH profile = token==address(0), ERC-20 via transferFrom, 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_protocol v1.2) + x402 accepts[] multi-token envelope.

Agent wallet (Python)AgentWallet/Treasury, session-key SpendPolicy delegation, a load-balancing Router (token / rail / fleet / rebalance), a fairness + agent-access-policy engine (per-agent token-bucket, tiers, contract-compliance), and escrow-fulfilment metrics.

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 chainsadapters/hanzo.py (Hanzo MCP fetch interop + HanzoAgentWallet), thinking_chain.py (ThinkingChain + HanzoEscrowThinkingChain), a watchable multi-token demo, and docs/thinking-chains-and-switchboard.md.

Verification

  • Python: 635 passed / 62 skipped / 0 failed
  • Solidity (Foundry): 67 passed / 0 failed
  • Demo: python3 examples/multitoken_thinking_chain_demo.py runs end-to-end (shows a chain halting correctly on a policy denial before any funds move).

For @abhicris — decisions to make (review agenda)

  1. §3.3 contract-generalization (A/B/C) — built as A (sibling contract). Must be on record before the EIP PR; if B/C, the ERC's Reference Implementation section changes.
  2. ERC-165 / EIP prepsupportsInterface(0x01dc5a49) added; EIP still needs discussions-to URL + handle verification before ethereum/EIPs.
  3. Rebasing tokens gated only by allowlist; native ETH intentionally not allowlist-gated.
  4. Swap: slippage anchored to oracle (independent post-swap re-check); staleness window; should 50 bps be a contract floor/ceiling?; fee-on-transfer swap path not yet explicitly tested.
  5. pay() is synchronous single-step (Router wired in) — confirm whether the wallet happy-path should honor the on-chain challenge window.
  6. daily_cap per session-key vs per-token; set_reserve is a spendable floor, not a hard debit lock.
  7. Negotiation rank additive/symmetric, tiebreak = lowest token address.
  8. x402 shape: switchboard nests terms under payment_requirements; x402.org v2 / Hanzo expect top-level accepts[] (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

  • Frontend: open web/onboarding.html
  • Metrics dashboard: web/metrics.html
  • Thinking-chain demo: python3 examples/multitoken_thinking_chain_demo.py

🤖 Generated with Claude Code

Pattermesh and others added 30 commits July 1, 2026 17:46
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)
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>
Pattermesh and others added 14 commits July 2, 2026 07:37
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>
Comment thread web/onboarding.html
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."""
...
Comment thread tests/test_cli.py
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
Comment thread tests/test_cli.py
def test_balance_all_tokens(self):
wallet = _patched_wallet(balance=100_000, token=USDC)
delegation = Delegation(wallet=wallet)
import switchboard.cli as cli_mod
Comment thread tests/test_cli.py

def test_balance_output_is_json(self):
wallet = _patched_wallet(balance=1_000, token=USDC)
import switchboard.cli as cli_mod
Comment thread tests/test_cli.py

class TestWalletGrant:
def _grant(self, *extra_args):
import switchboard.cli as cli_mod
Comment thread tests/test_cli.py

class TestWalletRevoke:
def test_revoke_valid_key(self):
import switchboard.cli as cli_mod
Comment thread tests/test_cli.py
cli_mod._delegation = None

def test_revoke_unknown_key_exits_nonzero(self):
import switchboard.cli as cli_mod
Comment thread tests/test_cli.py

class TestEscrowCreate:
def _setup(self):
import switchboard.cli as cli_mod
Comment thread tests/test_cli.py
return key.key_id

def _teardown(self):
import switchboard.cli as cli_mod
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants