```mermaid
sequenceDiagram
  autonumber
  participant C as wallet or banking (consumer)
  participant S as signer :8797
  participant P as person's browser
  C->>S: POST /v1/requests (Bearer consumer secret) intent register|auth, expiresAt, expectedAddress, approval
  S-->>C: requestId, approvalUrl (/bridge/privy?request_id&state), status pending, expiresAt
  C-->>P: approval link (via tool answer approval_url)
  P->>S: GET /v1/approvals/:id?state=...
  S-->>P: sanitized descriptor + consumer resultPublicKey
  P->>P: Privy root wallet signs
  P->>S: POST /v1/approvals/:id/result {state, envelope sealed to consumer key}
  S-->>P: status submitted, digest
  C->>S: GET /v1/requests/:id (status) / GET .../result
  S-->>C: envelope, digest (retained 5 min)
  C->>C: decrypt, verify, persist
  C->>S: POST /v1/requests/:id/ack {accepted:true, digest}
  S-->>C: status consumed (envelope deleted)
```

# Signer service (signer.paymentrequired.com) — not an MCP tool

[← Index](index.md) · [Overview](overview.md) · [Issued links](links.md)

> Evidence labels: **CODE** = read in the cited file at the recorded revision/hash; **FIXTURE** = parent-accepted combined cloud fixture run 99/99 (`directory-wave/parent-directory-wave-acceptance.json`), not live; **LIVE-RO** = observed read-only on the live host 2026-10-14 (raw files in `evidence/live-probes/`); **UNVERIFIED** = not executed here. Topology = accepted directory-wave source, **not deployed**. Nothing in this handbook moved money, signed, or activated a card.

## What it is

The signer shows a person what their agent wants approved; the person signs with their Privy **root** wallet; the result goes back **sealed** to the service that asked (wallet or banking). It holds no keys, no money and no product state (`signer:src/root-signer-broker.mjs:1`).
It has **no `/mcp`, no `tools/list`, no `tools/call`** (grep of `signer/repo/src` for `tools/list`, `tools/call`, `"/mcp"`: 0 hits); the apex directory lists it as `mcp: null` (`facade:src/directory.mjs:29`). An agent never calls it directly; it only sees an `approval_url` inside a `wallet`/`account`/`bank` answer.

**Source defect (report, not fixed here — sources are read-only):** the signer's own homepage text still says "Agents connect through the one MCP endpoint" `https://paymentrequired.com/mcp` (`signer:src/service-index.mjs:52`), which the directory retires (410). Wallet/banking copies of the same file were already corrected (`wallet:src/service-index.mjs:52`). Parent decides the fix.

## HTTP contract (exact routes)

| Who | Method + path | Body / query | Answer | Code |
|---|---|---|---|---|
| any | `GET /healthz` | – | `{ok, service:"root-signer", git_sha, status}` | `signer:src/root-signer-broker.mjs:141` |
| person | `GET /bridge/privy?request_id&state` | – | approval page | `signer:src/root-signer-broker.mjs:143` |
| person | `GET /v1/approvals/:id?state=` | – | descriptor + `resultPublicKey`, `signerAdapter:"privy"`; 410 if expired | `signer:src/root-signer-broker.mjs:119` |
| person | `POST /v1/approvals/:id/result` | `{state, envelope}` | `{status:"submitted", digest}` | `signer:src/root-signer-broker.mjs:128` |
| consumer (Bearer) | `POST /v1/requests` | `{intent: "register"\|"auth", privyAppId (root), expiresAt ≤24h, expectedAddress?, approval, requestId?, state?, returnOrigin?}` | `{requestId, approvalUrl, status:"pending", expiresAt}` | `signer:src/root-signer-broker.mjs:80` |
| consumer | `GET /v1/requests/:id` | – | `{requestId, status, expiresAt, resultDigest?}` | `signer:src/root-signer-broker.mjs:100` |
| consumer | `GET /v1/requests/:id/result` | – | `{envelope, digest, approvedAt, retainUntil}`; 409 unless submitted | `signer:src/root-signer-broker.mjs:104` |
| consumer | `POST /v1/requests/:id/ack` | `{accepted:true, digest}` | `{status:"consumed"}` | `signer:src/root-signer-broker.mjs:109` |
| consumer | `POST /v1/requests/:id/cancel` | `{}` | cancelled (pending only) | `signer:src/root-signer-broker.mjs:114` |

Constants: intents `signer:src/root-signer-envelope.mjs:15`, result retention 300 s `signer:src/root-signer-envelope.mjs:17`, max request 24 h `signer:src/root-signer-envelope.mjs:19`.
Status lifecycle (SQLite `signer_requests`): `pending → submitted → consumed`, or `pending → expired | cancelled` (`signer:src/root-signer-store.mjs:13`).
Consumers authenticate by bearer token whose hash is in `SIGNER_CONSUMERS_JSON`; a consumer sees only its own rows (`signer:src/root-signer-broker.mjs:66`).

## Who calls it

Wallet and banking, through `root-signer-client.mjs`, **only when `ROOT_SIGNER_URL` is set**; unset = the original in-process callback path (`banking:src/signer-broker-hook.mjs:23`, `banking:src/root-signer-client.mjs:38`). Remit never calls it.

## Money / safety

No money moves through the signer. A signature approves exactly what the consumer's `approval` descriptor says; the consumer verifies the decrypted result against the pinned owner before acting (`bridge.approved()` in the tool code).

## Observed

LIVE-RO 2026-10-14T18:09:13Z: `signer.paymentrequired.com` answers 503 "Service not deployed yet". FIXTURE (accepted 99/99): `banking_connect_link_on_signer` (probe-main) and the signer-down group 6/6 (probe-signerdown) passed with the final four source trees; synthetic keys, no real Privy. End-to-end with real Privy: **UNVERIFIED**.
