Skip to content

Commit 3319cc7

Browse files
Pattermeshclaude
andcommitted
Add live, watchable 2-agent ETH-escrow demo (mock chain)
Makes the existing examples/agentic_demo A2A flow watchable in a browser: two synthetic agents transact over an in-memory MockChain AgentEscrow — x402 402-offer -> validate(cap/allowlist/gas) -> escrow LOCK -> deliver -> confirm -> RELEASE -> SafeSwap route — replayed step by step from the REAL orchestration output (real escrow state, real X402Middleware spend summary, real SafeSwap receipt), not faked in JS. What's added (scenario.py / onchain.py / safeswap.py stay byte-for-byte unchanged): - observable.py — wraps run_scenario into an ordered, deterministic, render-ready timeline (logical clock + seeded ids => same seed yields byte-identical runs). - server.py — stdlib http.server transport: GET /, POST /api/demo/run, GET /api/demo/state, POST /api/demo/step, GET /healthz (DEMO.md API). - web/ — canonical page sources (index.html, demo.css, view.js, demo.js); build.mjs inlines them into the self-contained demo.html the server serves (drift-guarded by demo.test.mjs). - test_live.py + web/demo.test.mjs — pin timeline order, determinism, the HTTP envelope, and the page build-sync. - DEMO.md — fixed design contract; README.md gains a "Watch it live". How to run: PYTHONPATH=. python examples/agentic_demo/server.py # then open http://127.0.0.1:8402/ (or pass --open) SIMULATED / MOCK CHAIN — not a live network. No real ETH, no real RPC, no network, no funds; synthetic agents/keys only. Nothing here is a live or production deployment. The escrow / x402 / SafeSwap rail being shown is kcolbchain/switchboard's (the collective's agentic-payments rail; Abhishek Krishna / @abhicris leads); this layer only makes it watchable. Demo by Pattermesh (Patty / P. Sundaram). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1471365 commit 3319cc7

12 files changed

Lines changed: 4546 additions & 5 deletions

File tree

examples/agentic_demo/DEMO.md

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# Live Agentic-Escrow Demo — design contract
2+
3+
> **SIMULATED / MOCK CHAIN — not a live network.** Everything here runs against
4+
> an in-memory `MockChain` (see `onchain.py`) implementing the **AgentEscrow**
5+
> surface (`lock → confirm → release / refund`). **No real ETH, no real RPC, no
6+
> network, no funds.** The agents and keys are synthetic. Nothing in this folder
7+
> is a live or production deployment.
8+
>
9+
> _Demo by Pattermesh (Patty / P. Sundaram)_ on top of **kcolbchain/switchboard**
10+
> (the collective's agentic-payments rail; Abhishek Krishna / @abhicris leads).
11+
> The escrow + x402 + SafeSwap orchestration being shown is switchboard's, not
12+
> Patty's — this page only makes it **watchable**.
13+
14+
This document is the **fixed contract** between the three pieces of the live
15+
demo. The Architect owns this file and `server.py`'s stub signatures; the
16+
backend builder fills `observable.py` + the `server.py` handler bodies; the
17+
frontend builder writes the page that calls the API below. **Do not change the
18+
event field names, step ids, actor ids, or route strings** without updating all
19+
three.
20+
21+
---
22+
23+
## 1. What already exists (do NOT rewrite)
24+
25+
`examples/agentic_demo/` already runs the whole A2A flow in one shot via
26+
`scenario.run_scenario()`, which drives the **real** switchboard library
27+
(`switchboard.x402_middleware.X402Middleware`, `switchboard.gas_tracker.GasTracker`)
28+
against the mock substrate:
29+
30+
```
31+
402 offer → validate(cap/allowlist/gas) → escrow lock → deliver
32+
→ confirm/release → SafeSwap quote → SafeSwap execute
33+
```
34+
35+
`scenario.py`, `onchain.py`, `safeswap.py` stay **byte-for-byte unchanged**. The
36+
live demo *wraps* `run_scenario` to record an ordered, render-ready **timeline**,
37+
then serves it over a stdlib HTTP server. The existing `web/lab/swap.html` is a
38+
client-side cartoon with a hardcoded `STEPS` array; the live demo replaces those
39+
fake numbers with the **real** orchestration output (real escrow state, real
40+
SafeSwap quote, real spend summary).
41+
42+
### Canonical step ids (from `scenario.StepLog.step`, already matched by `swap.html`)
43+
44+
| seq | `step` id | meaning |
45+
|-----|----------------|------------------------------------------------------|
46+
| 0 | `setup` | Agent A funded; Agent B empty (pre-roll, UI may skip)|
47+
| 1 | `402` | Agent B → `402 Payment Required` + x402 escrow offer |
48+
| 2 | `validate` | Agent A: offer passes cap / allowlist / gas budget |
49+
| 3 | `pay` | Agent A locks funds in `AgentEscrow`**Locked** |
50+
| 4 | `deliver` | Agent B serves the work (`200 OK`) |
51+
| 5 | `settle` | Agent A `confirmPayment()` → escrow **Released** |
52+
| 6 | `swap.quote` | SafeSwap best-execution route quoted |
53+
| 7 | `swap.execute` | Routed swap settled → `SwapReceipt` |
54+
55+
These ids and their order are **load-bearing**: `tests/test_agentic_demo.py`
56+
already asserts `402 < pay < settle < swap.quote < swap.execute`, and the
57+
observable wrapper MUST reproduce the same ids in the same order.
58+
59+
---
60+
61+
## 2. The TIMELINE-EVENT model
62+
63+
A run is an **ordered list of `TimelineEvent`s** plus a small `summary`. One
64+
event per step above (the `setup` pre-roll included). Each event is a frame the
65+
UI can render to show the two agents transacting one step at a time: who acts,
66+
what moves, what the escrow + balances look like *after* the step, and which
67+
mock-chain block we're on.
68+
69+
### `TimelineEvent` (JSON object)
70+
71+
| field | type | notes |
72+
|----------------|------------------|----------------------------------------------------------------------------------------|
73+
| `seq` | int | 0-based order index (matches the table above). |
74+
| `step` | string | one of the canonical step ids. |
75+
| `phase` | string | coarse grouping for styling: `"setup" \| "pay" \| "swap"`. `swap.*``"swap"`. |
76+
| `actor` | string | the node that **initiates** this step: `"A" \| "B" \| "escrow" \| "safeswap" \| "system"`. |
77+
| `peer` | string \| null | the counterparty / destination node id (same vocabulary as `actor`), or `null`. |
78+
| `title` | string | short label, e.g. `"402 Payment Required"`. |
79+
| `detail` | string | one-line human description (plain text; UI may bold/format). |
80+
| `amount` | object \| null | value moved **this step** (see **Amount** below), or `null` if nothing moves. |
81+
| `escrow` | object \| null | escrow snapshot **after** this step (see **Escrow** below); `null` before lock. |
82+
| `balances` | object | balances **after** this step: see **Balances** below. |
83+
| `block` | int | `MockChain.block_number` **after** this step (mock-chain "block height"). |
84+
| `tx` | string \| null | tx hash / escrow `request_id` produced this step (`0x…` or a uuid), else `null`. |
85+
| `data` | object | step-specific extras, passthrough of `StepLog.data` plus the typed fields below. |
86+
87+
#### Amount (`event.amount`)
88+
89+
```jsonc
90+
{ "token": "USDC", "units": 5000000, "decimals": 6, "display": "5.00" }
91+
```
92+
93+
* `units` is the integer base-unit amount (USDC = 6dp, ETH = 18dp), exactly as
94+
the library/`MockChain` track it — never a float.
95+
* `display` is the human string the UI shows; computed as `units / 10**decimals`.
96+
97+
#### Escrow (`event.escrow`)
98+
99+
```jsonc
100+
{ "request_id": "", "state": "Locked", "amount": { /* Amount */ }, "payer": "A", "payee": "B" }
101+
```
102+
103+
`state` is one of `EscrowState` values: `"Locked" | "Confirmed" | "Released" |
104+
"Refunded" | "Cancelled"` (from `onchain.EscrowState`).
105+
106+
#### Balances (`event.balances`)
107+
108+
Map of **node id → Amount**, reflecting on-chain balances after the step:
109+
110+
```jsonc
111+
{ "A": { "token": "USDC", "units": 95000000, "decimals": 6, "display": "95.00" },
112+
"B": { "token": "USDC", "units": 5000000, "decimals": 6, "display": "5.00" } }
113+
```
114+
115+
After `swap.execute`, Agent B's balance Amount switches `token`/`decimals` to the
116+
swap output asset (e.g. `ETH`, 18dp) — mirroring `swap.html`'s `balBUnit` flip.
117+
118+
### Run envelope (`POST /api/demo/run` → 200)
119+
120+
```jsonc
121+
{
122+
"ok": true,
123+
"sandbox": "SIMULATED / MOCK CHAIN — not a live network. No real ETH/RPC/funds.",
124+
"deterministic": true,
125+
"params": { "price_units": 5000000, "swap_to": "ETH", "seed": 42 },
126+
"agents": {
127+
"A": { "id": "A", "role": "payer", "label": "Agent A", "address": "0x…" },
128+
"B": { "id": "B", "role": "payee", "label": "Agent B", "address": "0x…" },
129+
"escrow": { "id": "escrow", "role": "contract", "label": "AgentEscrow" },
130+
"safeswap": { "id": "safeswap", "role": "orchestrator", "label": "SafeSwap" }
131+
},
132+
"timeline": [ /* TimelineEvent, in seq order */ ],
133+
"summary": {
134+
"settled": true,
135+
"swap_routed": true,
136+
"escrow_request_id": "",
137+
"escrow_state": "Released",
138+
"offer": { "amount_units": 5000000, "currency": "USDC", "scheme": "escrow", "recipient": "0x…" },
139+
"swap": { /* SwapReceipt.to_dict() */ },
140+
"spend_summary": { /* X402Middleware.get_spend_summary() */ },
141+
"blocks_mined": 4
142+
}
143+
}
144+
```
145+
146+
`summary` is derived from the real `ScenarioResult` (`settled`, `swap_routed`,
147+
`spend_summary`, `swap_receipt.to_dict()`), so the page footer shows genuine
148+
library output, not recomputed JS.
149+
150+
On error the envelope is `{ "ok": false, "error": "<message>" }` with HTTP 400/500.
151+
152+
---
153+
154+
## 3. Determinism
155+
156+
The page must be **replayable to the same bytes** so the demo and its tests are
157+
stable. `scenario.run_scenario()` itself uses `time.time()` (offer `nonce` /
158+
`expires_at`, `SwapReceipt.settled_at`) and `uuid4()` (escrow `request_id`, tx
159+
hashes) — non-deterministic. The observable wrapper therefore runs in a
160+
**seeded / logical-clock** mode:
161+
162+
* A **logical clock** starts at a fixed epoch and advances by a fixed tick per
163+
recorded event, so `expires_at`, `settled_at`, and event `at` are reproducible.
164+
* A **seeded RNG** (default `seed=42`) derives the escrow `request_id` and any
165+
ids the wrapper itself mints, so `tx`/`request_id` are stable across runs.
166+
* The **mock-chain block numbers** are already deterministic (every
167+
`transfer`/`escrow_*` calls `mine()`), so `block` is stable for free.
168+
169+
Determinism is opt-outable (`deterministic=False` / no seed) for an organic live
170+
run, but the **default** for the server + all tests is deterministic. The
171+
contract: **two `POST /api/demo/run` with identical params + seed return
172+
byte-identical `timeline` + `summary`** (this is the headline backend test).
173+
174+
The wrapper **must not** edit `scenario.py`; it injects determinism by
175+
constructing its own `MockChain` / seeded clock and recording the same step
176+
sequence `run_scenario` performs (it may call `run_scenario(chain=…, safeswap=…)`
177+
and post-process its `StepLog`s, or re-drive the identical calls — builder's
178+
choice — but the resulting step ids/order MUST match §1).
179+
180+
---
181+
182+
## 4. HTTP API (stdlib `http.server`, no framework)
183+
184+
All JSON, `Content-Type: application/json`, permissive same-origin. Pure
185+
`http.server.BaseHTTPRequestHandler`**no Flask/FastAPI, no build step**, house
186+
style. Bind `127.0.0.1` by default.
187+
188+
| Method + path | body ||
189+
|------------------------|---------------------------------------|------------------------------------------------------|
190+
| `GET /` || serves the demo HTML page (text/html). |
191+
| `GET /api/demo/state` || the **last** run envelope, or a fresh seeded run if none yet. |
192+
| `POST /api/demo/run` | `{ "price_units"?, "swap_to"?, "seed"?, "deterministic"? }` | runs a full scenario; returns the **run envelope** (§2). |
193+
| `POST /api/demo/step` | `{ "reset"? : bool }` | **optional** stepwise mode: advance one event and return `{ "ok", "done", "index", "event": TimelineEvent }`; `reset:true` rewinds to before `setup`. |
194+
| `GET /healthz` || `{ "ok": true }` liveness. |
195+
196+
* `price_units` integer base units (default `5_000_000` = 5 USDC); `swap_to`
197+
`{"ETH","LUX","USDC"}` (default `"ETH"`); `seed` int (default `42`);
198+
`deterministic` bool (default `true`).
199+
* `POST /api/demo/run` is the primary path: the page calls it once and animates
200+
the returned `timeline` client-side (it already has the canvas + ledger from
201+
`swap.html`). `POST /api/demo/step` is a convenience for a true server-driven
202+
step mode; if unimplemented it returns `501`.
203+
* `GET /api/demo/state` lets a freshly-loaded page render the last run (or a
204+
default seeded run) without re-POSTing — handy for the PWA offline shell.
205+
206+
### Static page serving
207+
208+
`GET /` returns an HTML page. The server resolves it from (in order):
209+
210+
1. `examples/agentic_demo/demo.html` if present (a self-contained live page the
211+
frontend builder may add), else
212+
2. the repo's `web/lab/swap.html` (the existing visualization), rewriting its
213+
hardcoded `STEPS`/balances to fetch `/api/demo/run` instead.
214+
215+
Either way the page is **vanilla ES + the existing `shared.css` tokens** (gold
216+
`#d4a853`, emerald `#4ecb71`, violet `#a78bfa`, cyan `#67d4e0`, mono
217+
`JetBrains Mono`). No external requests beyond what `swap.html` already declares.
218+
219+
---
220+
221+
## 5. Contract surface the builders implement
222+
223+
These names are **fixed** by `server.py`'s stubs. Backend builder fills the
224+
bodies; frontend builder calls the routes.
225+
226+
**`examples/agentic_demo/observable.py`** (new; the timeline recorder — wraps
227+
`run_scenario`, never edits it):
228+
229+
```python
230+
@dataclass
231+
class TimelineEvent: # the §2 object; .to_dict() emits the JSON above
232+
seq: int; step: str; phase: str; actor: str; peer: str | None
233+
title: str; detail: str
234+
amount: dict | None; escrow: dict | None; balances: dict
235+
block: int; tx: str | None; data: dict
236+
def to_dict(self) -> dict: ...
237+
238+
@dataclass
239+
class DemoRun: # the run envelope (§2)
240+
params: dict; agents: dict; timeline: list[TimelineEvent]; summary: dict
241+
def to_dict(self) -> dict: ...
242+
243+
def run_observable(*, price_units: int = 5_000_000, swap_to: str = "ETH",
244+
seed: int = 42, deterministic: bool = True) -> DemoRun: ...
245+
# drives the real scenario, returns the recorded, render-ready timeline.
246+
247+
class StepCursor: # backs POST /api/demo/step (optional stepwise mode)
248+
def __init__(self, run: DemoRun): ...
249+
def reset(self) -> None: ...
250+
def advance(self) -> tuple[bool, int, TimelineEvent | None]: ... # (done, index, event)
251+
```
252+
253+
**`examples/agentic_demo/server.py`** (skeleton fixed by Architect; bodies by
254+
backend builder):
255+
256+
```python
257+
SANDBOX_NOTE = "SIMULATED / MOCK CHAIN — not a live network. No real ETH/RPC/funds."
258+
259+
class DemoHandler(BaseHTTPRequestHandler):
260+
def do_GET(self): ... # routes: / , /api/demo/state , /healthz
261+
def do_POST(self): ... # routes: /api/demo/run , /api/demo/step
262+
# helpers (fixed names):
263+
def _send_json(self, obj, status=200): ...
264+
def _send_html(self, html, status=200): ...
265+
def _read_json_body(self) -> dict: ...
266+
def _serve_page(self): ... # resolves the §4 static page
267+
def _handle_run(self, body: dict): ... # → run_observable → envelope
268+
def _handle_state(self): ... # last run or default seeded run
269+
def _handle_step(self, body: dict): ... # StepCursor; 501 if unimplemented
270+
271+
def make_server(host="127.0.0.1", port=8402) -> ThreadingHTTPServer: ...
272+
def main(argv: list[str] | None = None) -> int: ... # CLI: --host --port --open
273+
```
274+
275+
The frontend calls only the §4 routes; it never imports Python. The whole thing
276+
launches with `PYTHONPATH=. python examples/agentic_demo/server.py` and is then
277+
viewable at `http://127.0.0.1:8402/`.
278+
279+
---
280+
281+
## 6. Tests the backend builder must turn green
282+
283+
Scoped to `tests/test_agentic_demo_live.py` (new file; does NOT touch the
284+
existing `test_agentic_demo.py`). At minimum:
285+
286+
* **determinism** — two `run_observable(seed=42)` calls produce equal
287+
`to_dict()` (timeline + summary byte-identical).
288+
* **step coverage + order** — the timeline contains exactly the §1 step ids in
289+
order; `402 < pay < settle < swap.quote < swap.execute`.
290+
* **truthful amounts**`pay` shows the escrow `Locked` at the offer amount;
291+
`settle` shows escrow `Released` and Agent B credited the price; balances after
292+
`settle` are A=95 / B=5 USDC (units), matching `test_agentic_demo`'s ledger
293+
assertions; after `swap.execute` B's balance Amount flips to the swap asset.
294+
* **summary is real**`summary.settled is True`, `summary.swap_routed is True`,
295+
`summary.spend_summary["total_spent_wei"] == price_units`,
296+
`summary.swap.amountOut > 0`.
297+
* **HTTP** — spin up `make_server` on an ephemeral port; `POST /api/demo/run`
298+
returns `ok:true` + a non-empty `timeline`; `GET /healthz``ok:true`;
299+
unknown path → `404`; `GET /``200 text/html`.
300+
301+
`scenario.py`/`onchain.py`/`safeswap.py` and `test_agentic_demo.py` remain
302+
unchanged; the live layer is purely additive.

examples/agentic_demo/README.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@ PYTHONPATH=. python examples/agentic_demo/run.py --swap-to LUX --price 8 --json
1919
Exit code is `0` only when **both** `402 offer -> pay -> settle` and the
2020
**agentic swap** succeed.
2121

22+
## Watch it live (browser)
23+
24+
The same orchestration, made **watchable** — two agents, the x402 `402` offer, and
25+
the escrow `lock → release` animated step by step on the **mock chain**:
26+
27+
```bash
28+
PYTHONPATH=. python examples/agentic_demo/server.py
29+
# then open http://127.0.0.1:8402/ (or pass --open)
30+
```
31+
32+
> **SIMULATED / MOCK CHAIN — not a live network.** No real ETH, no RPC, no funds;
33+
> synthetic agents only. The page replays the *real* `run_scenario` output (real
34+
> escrow state, real `X402Middleware` spend summary, real SafeSwap receipt) — it
35+
> does not fake the numbers. `scenario.py` / `onchain.py` / `safeswap.py` are
36+
> unchanged; `observable.py` records the timeline and `server.py` serves it over
37+
> the JSON API in [`DEMO.md`](DEMO.md). _Live demo by Pattermesh (Patty /
38+
> P. Sundaram)_ on **kcolbchain/switchboard** (Abhishek Krishna / @abhicris leads).
39+
40+
```
41+
POST /api/demo/run → run the scenario, return the ordered timeline + summary
42+
GET /api/demo/state → replay the last run (or a fresh seeded one)
43+
GET /healthz → {"ok": true}
44+
```
45+
2246
## The flow
2347

2448
```
@@ -62,14 +86,23 @@ routing.
6286
| `scenario.py` | the orchestration + `BudgetGuard` + `AgentBEndpoint` |
6387
| `onchain.py` | `MockChain` ledger + escrow + `MockPaymentClient` (PaymentClient surface) |
6488
| `safeswap.py` | `SafeSwapClient` + `MockSafeSwapOrchestrator` |
89+
| `observable.py` | wraps `run_scenario` into an ordered, render-ready **timeline** (deterministic) |
90+
| `server.py` | stdlib HTTP server: the `/api/demo/*` JSON API + the live page |
91+
| `web/` | canonical page sources (`index.html`, `demo.css`, `view.js`, `demo.js`); `build.mjs` inlines them to the self-contained `demo.html` the server serves |
92+
| `DEMO.md` | the fixed design contract (event model, step ids, HTTP API) |
6593

6694
## Test
6795

6896
```bash
69-
PYTHONPATH=. python -m pytest tests/test_agentic_demo.py -q
97+
# scenario + the live (observable / server) layer
98+
PYTHONPATH=. python -m pytest tests/test_agentic_demo.py examples/agentic_demo/ -q
99+
# the page's pure view helpers + the demo.html build-sync guard
100+
node --test examples/agentic_demo/web/demo.test.mjs
70101
```
71102

72-
Asserts: the 402 offer carries the escrow scheme + price, the escrow ends
73-
`Released` (not just `Locked`), funds move payer → escrow → payee, the SafeSwap
74-
orchestrator is genuinely called (`quote` then `execute`), and the swap routes
75-
with a non-empty venue path and positive output.
103+
The scenario tests assert: the 402 offer carries the escrow scheme + price, the
104+
escrow ends `Released` (not just `Locked`), funds move payer → escrow → payee, the
105+
SafeSwap orchestrator is genuinely called (`quote` then `execute`), and the swap
106+
routes with a non-empty venue path and positive output. The live-layer tests
107+
(`examples/agentic_demo/test_live.py`) additionally pin the timeline order,
108+
determinism (same seed ⇒ byte-identical), and the HTTP API envelope.

0 commit comments

Comments
 (0)