Skip to content
Open
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
189 changes: 189 additions & 0 deletions docs/PWA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Switchboard Lab PWA + the "Switchboard Plugin" Proposal

**Status:** Draft v1
**App shell:** [`web/manifest.json`](../web/manifest.json) · [`web/sw.js`](../web/sw.js)
**Registration:** [`web/lab/shared.js`](../web/lab/shared.js) (lab pages) · [`web/index.html`](../web/index.html) (root)
**Verified by:** [`tests/test_pwa.py`](../tests/test_pwa.py)

---

## 1. Why a PWA

The Switchboard Lab is a static, no-build set of pages. Making it a **Progressive
Web App** gets us three things at zero infra cost:

1. **Installable** — agents-payments demos sit one tap away on a phone home screen
or a desktop dock, in a standalone window with no browser chrome.
2. **Offline-capable** — the whole lab (every scene, the canvas, the docs) loads
with no network. Good for conference Wi-Fi, planes, and demo booths.
3. **A distribution surface** — the same app shell is the reference for embedding
Switchboard payments into a *third-party* PWA (see §4, the "switchboard plugin").

This is intentionally additive: the lab still works as plain static HTML if the
service worker never registers (e.g. served from `file://`).

## 2. What shipped

| File | Role |
|------|------|
| `web/manifest.json` | Web App Manifest — name, icons, `start_url`, `display: standalone`, shortcuts |
| `web/icon.svg`, `web/icon-maskable.svg` | Vector icons (`any` + `maskable` purposes), obsidian + gold brand mark |
| `web/sw.js` | Service worker — precache app shell, offline navigation fallback, runtime caching |
| `web/index.html` | Root registers the SW at scope `./` + links the manifest |
| `web/lab/shared.js` | Every lab page registers the SW (`../sw.js`) and injects the manifest link |

### Manifest highlights

- `scope: "./"` and `start_url: "./lab/index.html"` — the installed app opens on
the lab dashboard but controls the entire `web/` tree.
- `display: "standalone"` with `display_override` falling back to `minimal-ui`.
- `shortcuts` — long-press / right-click the installed icon jumps straight to
**Agentic Pay + Swap**, the **Canvas Lab**, or the **x402 Paywall** scene.
- `theme_color` / `background_color` `#06060b` match the lab's obsidian splash so
the launch transition is seamless.

## 3. Install flow

```
┌──────────────┐ browser detects ┌──────────────────┐ user installs ┌────────────────┐
│ open lab in │ manifest + SW + │ install prompt │ ───────────────▶ │ standalone app │
│ Chrome/Edge │ ─ HTTPS + icons ────▶ │ (omnibox / menu) │ │ on home / dock │
└──────────────┘ └──────────────────┘ └────────────────┘
│ │
│ first load: SW `install` event precaches the app shell ──────────────────────┘
│ subsequent loads: served from cache, revalidated in the background
works fully offline (every scene + docs)
```

**Manual install:**
- **Desktop Chrome/Edge:** open `web/lab/index.html` over HTTPS (or `localhost`),
click the install icon in the address bar (or *⋮ → Install Switchboard*).
- **Android Chrome:** *⋮ → Add to Home screen / Install app*.
- **iOS Safari:** *Share → Add to Home Screen* (Safari reads the manifest name +
`apple-touch-icon`; service-worker offline support applies in standalone mode).

**Local dev / verifying offline:**

```bash
cd web && python3 -m http.server 8731
# open http://localhost:8731/lab/index.html, let it load once,
# then DevTools → Network → Offline, and reload — the lab still renders.
```

> Service workers require a secure context: `https://` **or** `http://localhost`.
> Served from `file://`, the lab degrades gracefully to plain static pages
> (registration is guarded with `location.protocol !== 'file:'`).

## 4. Offline architecture

The service worker (`web/sw.js`) uses three strategies keyed off the request:

1. **App-shell precache** (`install`): the lab pages, `shared.css`/`shared.js`, the
icons, and the root pages are fetched with `cache: "reload"` and stored under a
versioned cache (`switchboard-lab-v1`). Adds are resilient — a single 404 does
not abort the whole install.
2. **Navigations → network-first** with a cache fallback, then the offline shell
(`./lab/index.html`). So a brand-new route works online and a previously-visited
route works offline.
3. **Same-origin assets → stale-while-revalidate**; **cross-origin (Google Fonts)
→ cache-first** so type renders offline.

`activate` deletes stale caches; bump `CACHE_VERSION` on deploy to invalidate. A
page can post `SKIP_WAITING` to adopt a new worker immediately.

## 5. The "switchboard plugin" PWA proposal

The lab PWA doubles as the **reference embedding** for shipping Switchboard
payments inside *someone else's* PWA — a wallet, a marketplace, an agent console.
The idea: a drop-in **"switchboard plugin"** an app installs once and then calls to
gate features behind agent payments.

### 5.1 Shape

```
host PWA (installed)
├─ <script type="module" src="switchboard-plugin.js"></script>
│ registers a SECOND service worker scoped to /pay/*
│ (or a module imported by the host SW) that:
│ • intercepts fetches that come back 402
│ • parses the x402 PaymentRequirements (switchboard/x402)
│ • drives the on-chain pay/escrow flow
│ • retries with the X-Payment proof header
└─ UI: an install-time permission ("allow agentic payments up to N USDC/day")
backed by the gas-budget primitive (switchboard.gas_tracker)
```

The plugin reuses the exact wire types the Python library defines so host and
agent speak the same protocol:

- **402 challenge / proof** — `switchboard/x402/server.py`
(`PaymentRequirements`, `X-Payment` / `X-Payment-Proof`, `WWW-Authenticate: x402`).
- **Escrow settlement** — `src/payment_protocol.py` + `contracts/AgentEscrow.sol`.
- **Spend caps** — `switchboard.gas_tracker.GasTracker` enforces the per-hour /
per-day budget the user grants at install.
- **Agentic swap** — after receiving funds, route through SafeSwap exactly as in
[`examples/agentic_demo`](../examples/agentic_demo/) (`SafeSwapClient`).

### 5.2 Install + consent flow

```
1. user installs the host PWA (manifest + SW)
2. host PWA imports the switchboard plugin
3. plugin shows a one-time consent sheet:
"Switchboard may pay agents on your behalf, up to 20 USDC/day,
only to recipients you approve. Funds settle on-chain via escrow."
4. consent persists the budget + allowlist (IndexedDB)
5. from then on, any fetch the host makes that returns 402 is auto-paid
within budget — fully offline-first for the UI, on-chain for settlement
```

This mirrors the policy gate already implemented server-agnostically in
`X402Middleware._validate_offer()` (cap, recipient allowlist, gas budget) — the
plugin is that check, moved into the browser.

### 5.3 How the plugin embeds switchboard payments

A host page gates a paid feature with a single call:

```js
import { switchboardPay } from './switchboard-plugin.js';

// fetch a paid agent endpoint; the plugin handles the 402 → pay → retry loop
const res = await switchboardPay('https://agent-b.example/v1/inference', {
method: 'POST',
body: JSON.stringify(job),
// policy comes from install-time consent; can be tightened per call
maxUsd: 5,
allow: ['0xB0b0…'],
});
```

Under the hood that is the browser twin of the Python demo: parse the offer,
validate against the budget, settle into escrow, retry with proof — and
optionally route the proceeds through SafeSwap. The lab's
[Agentic Pay + Swap scene](../web/lab/swap.html) is the visual spec for exactly
this loop.

### 5.4 Roadmap

| Step | Deliverable |
|------|-------------|
| 1 | `switchboard-plugin.js` — `switchboardPay()` + 402 interception (ships the §5.3 API) |
| 2 | Consent sheet + IndexedDB-backed budget/allowlist (the in-browser `GasTracker`) |
| 3 | Wallet binding (EIP-1193 / EIP-7702 smart-account) for real on-chain settlement |
| 4 | SafeSwap routing of received funds, surfaced as an optional auto-rebalance |
| 5 | Publish alongside [`@kcolbchain/eliza-switchboard`](../packages/plugin-switchboard) as a browser counterpart |

## 6. Verification

```bash
PYTHONPATH=. python -m pytest tests/test_pwa.py -q
```

Asserts the manifest is valid + complete, its icons and `start_url`/shortcut
targets exist, the service worker is valid JS that precaches a real on-disk app
shell with an offline fallback, and that registration is wired into both the root
page and every lab page.
75 changes: 75 additions & 0 deletions examples/agentic_demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Agentic Payments Demo — A2A pay + escrow settle + SafeSwap route

A runnable scenario where **Agent A pays Agent B for work** via the Switchboard
x402 middleware + on-chain escrow, settles on delivery, then **Agent B routes the
received token through [SafeSwap](#safeswap)** to rebalance into a target asset.

Everything runs **offline** — no RPC node, no live SafeSwap — by driving the real
`switchboard` package surface against an in-memory chain and an in-process SafeSwap
mock. Swapping in a live RPC `PaymentClient` and `SafeSwapClient(base_url=...)`
needs no changes to the scenario code.

## Run it

```bash
PYTHONPATH=. python examples/agentic_demo/run.py
PYTHONPATH=. python examples/agentic_demo/run.py --swap-to LUX --price 8 --json
```

Exit code is `0` only when **both** `402 offer -> pay -> settle` and the
**agentic swap** succeed.

## The flow

```
Agent A Agent B (paid endpoint) SafeSwap
│ GET /inference ───────────▶ │
│ ◀─────────── 402 + x402 PaymentOffer (escrow, 5 USDC) │
│ validate offer (cap / allowlist / gas budget) │
│ lock 5 USDC in AgentEscrow ──▶ [Locked] │
│ ◀─────────── 200 OK + deliverable │
│ confirmPayment() ──────────▶ escrow [Released] → B paid │
│ route 5 USDC ─────────────────▶│ quote
│ ◀────────────── best route + amountOut
│ execute ─────────────────────▶ │ settle
│ ◀────────────── SwapReceipt │
```

1. **402 offer** — `AgentBEndpoint.offer()` returns a real
`switchboard.x402_middleware.PaymentOffer` with the **escrow** scheme.
2. **validate + pay** — Agent A's `X402Middleware._validate_offer()` enforces the
payment cap, recipient allowlist, and gas budget, then `_pay_onchain()` locks
funds via the escrow `create_payment()` path.
3. **deliver** — Agent B serves the work against the payment proof.
4. **settle** — Agent A `confirm_payment()` → escrow transitions
`Locked → Released`, crediting Agent B.
5. **agentic swap** — Agent B routes the received USDC through `SafeSwapClient`
(`quote` → `execute`) into ETH/LUX, getting a best-execution route + receipt.

## SafeSwap

`safeswap.py` is a tiny client against SafeSwap's orchestrator HTTP API
(`/v1/quote`, `/v1/execute`). It ships with `MockSafeSwapOrchestrator`, an
in-process transport with deterministic pricing so the demo and tests run with no
network. Point `SafeSwapClient(base_url=...)` at the live orchestrator for real
routing.

## Files

| File | Role |
|------|------|
| `run.py` | CLI entrypoint (`--swap-to`, `--price`, `--json`) |
| `scenario.py` | the orchestration + `BudgetGuard` + `AgentBEndpoint` |
| `onchain.py` | `MockChain` ledger + escrow + `MockPaymentClient` (PaymentClient surface) |
| `safeswap.py` | `SafeSwapClient` + `MockSafeSwapOrchestrator` |

## Test

```bash
PYTHONPATH=. python -m pytest tests/test_agentic_demo.py -q
```

Asserts: the 402 offer carries the escrow scheme + price, the escrow ends
`Released` (not just `Locked`), funds move payer → escrow → payee, the SafeSwap
orchestrator is genuinely called (`quote` then `execute`), and the swap routes
with a non-empty venue path and positive output.
34 changes: 34 additions & 0 deletions examples/agentic_demo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Agentic payments demo: Agent A pays Agent B via x402 + escrow, then B routes
the received token through SafeSwap.

Run with::

PYTHONPATH=. python examples/agentic_demo/run.py
"""

from .safeswap import (
MockSafeSwapOrchestrator,
SafeSwapClient,
SafeSwapError,
SwapQuote,
SwapReceipt,
SwapRequest,
)
from .onchain import EscrowState, MockChain, MockPaymentClient
from .scenario import AgentBEndpoint, ScenarioResult, StepLog, run_scenario

__all__ = [
"run_scenario",
"ScenarioResult",
"StepLog",
"AgentBEndpoint",
"MockChain",
"MockPaymentClient",
"EscrowState",
"SafeSwapClient",
"MockSafeSwapOrchestrator",
"SafeSwapError",
"SwapRequest",
"SwapQuote",
"SwapReceipt",
]
Loading
Loading