Skip to main content
Glama
README.md
# Between

**Your Alexa negotiates with other families' Alexas — so you don't have to send
the group text.**

Between gives each household an assistant that owns one job: covering the
things school and clubs drop on your calendar. You say *"I can't take Maya to
soccer on Thursday"*; your assistant works out who can cover it, negotiates a
fair swap with their assistants, and brings you one confirmable offer. Every
byte that crosses between homes is a signed, schema-checked offer — never your
calendar, never Maya's name.

Built for the **Alexa+ MCP toolkit hackathon** (October 2026).

---

## What the demo shows

Two kitchens, two assistants, one Thursday:

1. **Ask.** "I can't take Maya to soccer on Thursday. Can you sort it out?"
   A card shows the negotiation feed: three families asked, in plain language.
2. **Negotiate.** The Lees are busy (their assistant says no — with no
   explanation, because households don't get to see each other's calendars).
   The Parks counter with a swap; the Chens offer a plain favour. The Parks win
   because the Ahmads *owe* the Lees — and the ledger is the family's own.
3. **Approve.** One proposal card in each kitchen. *They take Thursday, you
   take Tuesday.* Each family taps yes, by voice or on the card. Nobody has
   accepted anything until both have.
4. **Settle.** Both calendars move, both ledgers move to net zero, and the wire
   panel shows every message that actually crossed the line: signed offers and
   answers, no titles, no names, no locations.

Both kitchens at the end of that run — the same moment, two households, captured
through the hosted link:

| Ahmad's kitchen | Park's kitchen |
|---|---|
| ![Ahmad's kitchen: the settled card, the calendar with park covers, and the wire](docs/img/ahmad-kitchen.png) | ![Park's kitchen: the settled card in Park's words and Park's own ledger](docs/img/park-kitchen.png) |

## How it works

```
        ahmad's house                park's house (+ lee, chen)
 ┌───────────────────────────┐   ┌───────────────────────────┐
 │  MCP server  (4 tools)    │   │  MCP server  (4 tools)    │
 │  ▼                        │   │                        ▲  │
 │  negotiation engine       │   │       negotiation engine  │
 │  ├ calendar (private)     │   │       calendar (private)  │
 │  ├ favour ledger (private)│   │       favour ledger       │
 │  └ threads / offers       │   │       threads / offers    │
 │        │                  │   │              │            │
 │        └── signed offers ─┼──►│  POST /peer/offers        │
 │  SQLite (one file)        │◄──┼── signed offers           │
 └───────────────────────────┘   └───────────────────────────┘
        ▲ MCP over HTTP                  ▲ MCP over HTTP
        │                                │
   Alexa+ (or the simulator page)   Alexa+ (or the simulator)
   renders the MCP App view          renders the MCP App view
```

- **The assistant surface is MCP.** Four tools: `find_help`,
  `negotiation_status`, `respond_to_proposal`, `ledger`, plus a `ui://` app
  resource so the assistant renders cards instead of walls of text.
- **Households talk only in offers.** [`between-protocol`](packages/protocol):
  strict Zod schema, Ed25519 signatures over the raw bytes, expiries, ±5-min
  clock skew, at most 3 counters per thread. Unknown fields are a hard error,
  so an offer cannot smuggle a calendar entry.
- **The engine is deterministic.** It fans out by who owes you, ranks moves
  (plain favours before swaps, cheapest first, ledger-aware), and runs a
  bounded next-move protocol when a swap doesn't fit. The optional
  Claude-on-Bedrock chooser picks among *already legal* moves and can be wrong
  without ever being unsafe; with `CHOOSER=rules` (the default) there is no
  cloud dependency at all.
- **Commitment is two-sided and idempotent.** A deal only lands after both
  families say yes; the commit into calendar + ledger is replay-safe.

## Run it

Requirements: Node 24+, npm. No account, keys or cloud needed.

```sh
npm install
npm run build      # builds the MCP App view and the simulator page
npm test           # protocol + engine + MCP surface + failure drills
npm run e2e        # four real servers, signed offers over HTTP, full scenario

npx playwright install chromium   # once, for the browser suites
npm run ui         # both kitchens in a real browser: cards, buttons, panels
npm run hosted     # the same pages, served the way judges get them (path prefixes)

npm run dev        # starts ahmad (4001), park (4002), lee (4003), chen (4004)
npm run host       # dev + one public ngrok link for judges, both kitchens behind it
```

Then open the simulated Alexa+ for the Ahmads:

**http://127.0.0.1:4001/sim/** — and the Parks:
**http://127.0.0.1:4002/sim/**

Say or type: *"I can't take Maya to soccer on Thursday"* in one window; watch
the other window speak up when the proposal lands. Each page carries the
side panels the demo narrates: the household calendar, the favour ledger, and
the raw wire — every message on the wire is a schema-valid signed offer.

### One link, both kitchens

`npm run host` puts the same demo on one public URL. A free ngrok account has a
single reserved domain and every tunnel grabs it, so the second kitchen would
fight the first over it; instead [`scripts/router.ts`](scripts/router.ts) serves
Ahmad at `/ahmad/…` and Park at `/park/…` behind one tunnel:

- `https://<your-domain>/ahmad/sim/` — Ahmad's kitchen
- `https://<your-domain>/park/sim/` — Park's kitchen

The script prints those links and writes them, with ready-made reset commands,
to `data/hosted-links.txt` — paste that into the Devpost testing instructions.
Keep that terminal open and the Mac awake while judges test: the demo runs on
this machine. ngrok free shows first-time browsers a one-click "Visit Site"
page; command-line requests are unaffected. A named Cloudflare tunnel on your
own domain would skip that page entirely.

`DEMO_NOW=2026-10-17T09:00:00-07:00` pins "today" so seeds, tests and recordings
are reproducible. `PACE_MS=1200` slows agent replies so a human can watch.
See [`.env.example`](.env.example) for every knob.

## What is real, and what is simulated

The honest table, because this is a hackathon build:

| Piece | Status |
| --- | --- |
| MCP server, tools, app view | **Real** — an MCP host drives it; the simulator talks to the same server over HTTP |
| Offer protocol, signatures, state machine | **Real** — [`between-protocol`](packages/protocol), published and independently testable |
| Negotiation engine, calendar, ledger | **Real** — SQLite per household, eventually consistent by signed messages |
| Alexa+ device | **Simulated page** — the toolkit is a US-only developer preview; the simulator hosts the real MCP App through the ext-apps app bridge |
| Claude-on-Bedrock chooser | **Optional** — ships disabled (`CHOOSER=rules`); it only ever ranks already-legal moves |

## Repo map

```
src/                    household server: engine, protocol transport, MCP, sim backend
web/app/                the MCP App view (feed, proposal, settled, ledger)
web/sim/                the simulated Alexa+ page (device, transcript, panels)
packages/protocol/      between-protocol: schema, signing, threads — README + SPEC
scripts/dev.ts          runs the four local families
scripts/host.ts         dev + ngrok + the front door, for judges
scripts/router.ts       one public URL, one path per kitchen
test/                   protocol, engine, MCP surface, four-server e2e, simulator
                        (test/ui.test.ts and test/hosted.test.ts drive real browsers)
docs/between.md         product spec and demo script
```

## Safety and privacy stance

- Only offers cross the wire: task label, start, duration. Nothing else, ever.
- A decline exposes **nothing** — not why, not what conflicts.
- Nothing is accepted on a family's behalf: both sides approve by voice.
- The engine can only execute moves it generated; the chooser ranks, it never
  invents. Misheard requests get a spoken next step, not a bad calendar write.

## License

MIT.