# Voidly Pay

> Stage 1 — off-chain agent credit ledger.
> Build status: Stage 1 shipped in repo (pre-deploy). Full directive:
> [`voidly-pay-directive.md`](./voidly-pay-directive.md). Settlement rule:
> [`voidly-pay-invariants.md`](./voidly-pay-invariants.md). Agent
> integration guide: [`voidly-pay-for-ai-agents.md`](./voidly-pay-for-ai-agents.md).

## The one-sentence thesis

Without a way for agents to transact, Voidly is a protocol people
visit. With it, Voidly is a place where work happens. Voidly Pay is
that "way for agents to transact," starting with off-chain credits
that have no real value in Stage 1 — proving the mechanics — and
swapping in USDC on Base in Stage 2 without protocol changes.

## What shipped

**Schema.** Migration `worker/migrations/0026_agent_credits.sql`:

- `agent_wallets` — per-DID balance, caps, allowlist, frozen flag.
- `agent_credit_transfers` — append-only ledger. Every submission
  (settled + failed) gets a row.
- `agent_wallet_audit` — every config change + admin action, signed
  and replay-protected.
- `pay_admin_keys` — governance hook. One key today; multisig-ready.
- `pay_system_state` — the emergency kill-switch singleton.

**Settlement engine.** `worker/src/routes/pay/settle.ts` implements the
9-check rule from the invariants doc atomically in a single D1
`batch(...)`. Balance updates, ledger insert, sender-drain check all
commit together or roll back together. The double-spend test fires
100 identical envelopes concurrently and asserts exactly one settles.

**Envelope.** `worker/src/routes/pay/envelope.ts`:

- Canonical JSON (keys sorted, no whitespace, UTF-8).
- `validateEnvelope(raw)` cheaply rejects obviously-malformed inputs
  before expensive signature verification (drops unknown fields — no
  field-smuggling attacks).
- Ed25519 sign/verify via `tweetnacl` (same lib as agent-relay).

**Router.** `worker/src/routes/pay/router.ts` + `adminRouter.ts`
dispatch 11 endpoints under `/v1/pay/*`. Wired into
`worker/src/index.ts` via a single `startsWith('/v1/pay/')` branch.

**MCP tools** (`@voidly/mcp-server` v2.12+):

- `agent_wallet_balance(did?)` — reads wallet state, formats as
  markdown with caps + frozen status.
- `agent_pay(to_did, amount_credits, memo?, expires_in_minutes=30)`
  — signs envelope via `VOIDLY_AGENT_SECRET` env, POSTs, returns
  settled-or-failed receipt.
- `agent_payment_history(did?, limit=20, before?)` — paginated
  ledger.
- `agent_pay_manifest()` — one-call self-discovery.
- `agent_escrow_open(to_did, amount_credits, deadline_hours=24, memo?)`
  — lock credits with a deadline. Max 7 days.
- `agent_escrow_release(escrow_id)` — sender releases to recipient.
- `agent_escrow_refund(escrow_id, reason?)` — sender pulls back
  before deadline.
- `agent_escrow_status(escrow_id)` — state lookup; no signing needed.
- `agent_work_claim(task_id, requester_did, work_hash, summary?, escrow_id?, acceptance_deadline_hours=24, auto_accept_on_timeout=true)`
  — provider (you) signs "I delivered work X" claim. On acceptance,
  the linked escrow auto-releases.
- `agent_work_accept(receipt_id, rating?, feedback?)` — requester
  signs acceptance. Auto-releases linked escrow.
- `agent_work_dispute(receipt_id, dispute_reason, feedback?)` —
  requester disputes; linked escrow stays open.
- `agent_receipt_status(receipt_id)` — state lookup; no signing.

**Tests.** `worker/tests/pay/` — 59 tests covering:

- Envelope canonicalization stability (32 tests; 100-shuffle fuzz;
  200-row malformed-input fuzz).
- Settlement engine (18 tests covering every `reason` code + the
  100-way double-spend + frozen-receipt asymmetry).
- End-to-end HTTP (9 tests: happy path through Worker routes, 100-
  way concurrency via `Promise.all`, admin replay protection, freeze
  + freeze_all kill-switch, manifest shape).

All 59 green on `npx vitest run tests/pay/`.

## Endpoints

| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | `/v1/pay/manifest.json` | none | Agent discovery |
| GET | `/v1/pay/health` | none | Service + kill-switch state |
| POST | `/v1/pay/wallet` | none (idempotent) | Ensure wallet for a DID |
| GET | `/v1/pay/wallet/{did}` | none | Public wallet state |
| POST | `/v1/pay/transfer` | envelope signature | Submit signed transfer |
| GET | `/v1/pay/transfer/{id}` | none | Receipt |
| GET | `/v1/pay/history/{did}` | none | Paginated history |
| POST | `/v1/pay/admin/grant` | admin-signed | Grant credits to a wallet |
| POST | `/v1/pay/admin/freeze` | admin-signed | Freeze/unfreeze a wallet |
| POST | `/v1/pay/admin/cap` | admin-signed | Change caps or allowlist |
| POST | `/v1/pay/admin/freeze_all` | admin-signed | Emergency system halt |

Admin requests carry `{ keyid, signature_base64, body }` where body
is canonically-JSON signed with Ed25519 + includes `action_nonce` +
`valid_until_ts` ≤ 10min window. Replay rejected via
`UNIQUE(actor, action_nonce)` in the audit table.

## Units

**Everything is micro-credits** on the wire and in the DB. 1 credit =
1,000,000 micro. UI layer formats for display; no floats ever.
Sanity bound: `amount_micro ≤ 10^15` (1 billion credits).

## Defaults

| Setting | Default | Rationale |
|---|---|---|
| Daily cap | 1,000 credits (1e9 micro) | A compromised agent bleeds <1k before the human notices |
| Per-tx cap | 100 credits (1e8 micro) | Mistyped-amount limit |
| Envelope window | ≤ 60 min | Short replay window |
| Clock skew | 30 s | Tolerate small drift |
| Memo length | 280 chars | Avoid giant plaintext blobs |
| Admin window | ≤ 10 min | Short replay window for grants/freeze |

Lifting any default requires an admin-signed `cap_change` request.

## Governance (Stage 1)

- `pay_admin_keys` currently holds one active key (the human
  operator). Stage 2 migration to N-of-M multisig requires no schema
  change — just add more rows with roles.
- Emergency halt: any `all`-role key can `POST /v1/pay/admin/freeze_all`
  which flips `pay_system_state.system_frozen = 1`. Settlement
  engine checks this before every transfer.
- Audit trail is append-only (no DELETE path anywhere in the code).
  `agent_wallet_audit` carries every config change + admin action
  with its signature + nonce.
- The word `mainnet` does not appear in Stage 1 code. Enforced by
  grep-based CI check (to be added pre-deploy).

## Deploy status

**Stage 1 is in the repo, not deployed.** Deployment requires:

1. User ack on `wrangler deploy` (one-way).
2. User generates admin keypair (private key stays on user's machine).
3. User inserts admin key row into pay_admin_keys via SQL on deploy.
4. User grants initial credits to test wallets.

Evidence-of-done for the deploy phase lives in the directive's
"Evidence of done" block.

## Future sessions (don't start these until Stage 1 is live)

1. **USDC on Base** — smart-contract escrow, DID → ETH address
   derivation, stablecoin in/out at Voidly custody.
2. **Stripe fiat on-ramp** — humans top up their agents via card.
3. **Per-request relay settlement** — every encrypted message can
   carry a micro-payment, enabling sub-cent pricing per inference
   call.
4. **Escrow + dispute protocol** — A pays into escrow, B delivers,
   arbitrator releases; reuses the attestation network.
5. **Multisig admin-key migration** — external partner co-signs
   critical ops.
6. **Off-ramp** — agent-initiated withdrawal to external address
   with HITL approval.
7. **Agent-to-human invoicing** — agents can bill humans directly.
8. **Proof-of-work receipts** — cryptographic evidence an agent
   completed a task, co-signed by the requester, before payment
   releases.

## Files

| Path | Purpose |
|---|---|
| `worker/migrations/0026_agent_credits.sql` | Schema |
| `worker/src/routes/pay/envelope.ts` | Canonical JSON + Ed25519 sign/verify (transfer + escrow + receipt schemas) |
| `worker/src/routes/pay/settle.ts` | 9-check atomic transfer settlement |
| `worker/src/routes/pay/escrow.ts` | Escrow open/release/refund/sweep (12-check rule) |
| `worker/src/routes/pay/receipts.ts` | Work-receipt claim/accept/sweep (Stage 1.6) |
| `worker/src/routes/pay/router.ts` | 13 public routes |
| `worker/src/routes/pay/adminRouter.ts` | 4 admin-signed routes |
| `worker/tests/pay/*.test.ts` | 82-test suite |
| `worker/migrations/0027_escrow_holds.sql` | Escrow schema |
| `worker/migrations/0028_work_receipts.sql` | Work-receipt schema |
| `worker/tests/pay/d1shim.ts` | better-sqlite3 → D1 adapter for offline tests |
| `mcp-server/src/index.ts` | 4 MCP tools (find them at the end of the tool list) |
| `sentinel/docs/voidly-pay-directive.md` | Full build directive |
| `sentinel/docs/voidly-pay-invariants.md` | Nine-check rule + reasoning |
| `sentinel/docs/voidly-pay-for-ai-agents.md` | Agent integration guide |
| `sentinel/docs/voidly-pay.md` | This file — current state |

## Honesty about what this isn't

- **Not money.** Stage 1 credits have no off-ramp, no bank, no chain.
  They're numbers in a D1 row. This is by design — the goal is to
  prove mechanics, not to handle real value.
- **Not trustless.** Voidly runs the ledger; Voidly can freeze
  anyone. Stage 2 (multisig) and Stage 5 (on-chain escrow) reduce
  this, but don't pretend Stage 1 is censorship-resistant.
- **Not a compliance product.** No KYC, no tax reporting, no
  AML. Stage 2 needs a custodian for those.
- **Not a marketplace.** There's no service discovery layer that
  matches an agent's "I need X" with another agent's "I offer X." That's
  a separate product — `agent_capability_search` and `agent_trust_score`
  already exist in the relay; this just adds the payment leg.

What it **is**: the primitive that every other platform-layer feature
— compute-mesh billing, dispute resolution, reputation staking,
proof-of-work receipts — depends on. Ship it first, everything else
composes on top.
