# ROADMAP — Keysat

Longer-term backlog. Near-term state lives in `AGENTS.md` → Current state.

## Payments & subscriptions

- Per-profile SMTP override (schema fields exist from the keysat-smtp-emails plan; needs the form + send path).
- Rail-preference editing UI — only matters when two providers on one profile both serve the same rail; settable today via `PUT /v1/admin/merchant-profiles/:id/rail-preferences/:rail`.
- **Auto-charge silently lapses a subscription on a 200-with-failure response (money-path bug; elevated above the other parked payments items).** `try_auto_charge_zaprite` returns `Ok(true)` on *any* HTTP 2xx (`subscriptions.rs:1403-1410`), reading the order `status` only for a log line. If Zaprite returns 200 carrying a `FAILED`/`DECLINED`/`EXPIRED` order status, the daemon fires `auto_charge_initiated` and then waits for an `order.paid` webhook that never arrives — the subscription silently lapses, no error surfaced, the customer churns. Safe fix (no production data needed): treat any non-`PAID` terminal order status as not-success and fall through to the manual-pay path — a conservative fail-safe, ~10 lines + a mock-provider test. (Found during the 2026-06-17 adjudication; it replaces the old "harden Zaprite failure-body shapes" item, which was already satisfied for non-2xx responses — those route correctly to WARN + `auto_charge_failed` audit + webhook + manual-pay fallback.)
## Agent compatibility & scoped API keys

- **Agent-delegable payment-provider connect** (approved, not urgent — see
  `plans/agent-payment-connect-scope.md`). Add an à-la-carte `payment_providers:write` scope
  (never bundled into `merchant-onboard`), gated by a daemon-level **sandbox-mode flag** as the
  outer gate (production daemons reject scoped connect entirely) with a **network gate** inner
  defense (regtest/testnet/signet only, fail-closed to mainnet). BTCPay network is derived from
  an on-chain address prefix (no `server/info` field exists).
- **Onboarding doc-harness — Stage 2 (Path 2, regtest buyer-pays).** Gated on slices 3–5 above.
  Stage 1 (Path 1, no payments) shipped `completed-clean` this session — harness at
  `licensing-service-startos/onboarding-harness/`, record in its `STAGE1-RESULT.md`. Stage 2
  reuses the harness but boots the fixture with `KEYSAT_SANDBOX_MODE` on, stands up a Dockerized
  BTCPay regtest stack (bitcoind regtest + NBXplorer + Postgres + BTCPay) as additional
  disposable infra, and grants the agent `merchant-onboard` + `payment_providers:write`. Goal:
  the agent connects BTCPay (regtest) over the API and drives a test buyer payment that activates
  a license, with zero master-key steps. The walkthrough must be explicitly labeled
  regtest/test-network and must state that connecting a real mainnet wallet is the one
  operator-reserved step **by design** (a key that can redirect funds stays with the human) — a
  security feature, not a gap.

## Packaging & distribution

- Start9 Community Registry submission — a 2026-06-17 spec check found the wrapper passes the functional
  criteria (manifest, interfaces, health check, backup/restore, BTCPay dep, actions). Remaining gap before
  submission: add a `prepare.sh` to set up a clean Debian box for the first build (copy the one from
  `hello-world-startos`), then run the on-box manual verification (install / backup / restore / logs).
  Submission criteria themselves remain unpublished; reach out to Start9 when ready. (Icon-render and the
  source-available license are intentionally not treated as blockers.)

## Licensing model

- Evaluate Elastic License v2 vs the current custom `LicenseRef-Keysat-1.0` (parked decision).

## Validation

- Re-test `KEYSAT_INTEGRATION.md` against a fresh downstream app to confirm a clean one-shot SDK integration.
- **Add an automated regression test for multi-profile webhook routing** (adjudicated 2026-06-17 → DO, low blast radius — replaces the parked "manual Zaprite sandbox pass"). The routing is a deterministic provider-id→profile primary-key lookup with an anti-forgery re-fetch backstop, so the manual sandbox ceremony isn't worth it — but the path-keyed route (`/v1/{provider}/webhook/:provider_id` → `handle_for_provider`) currently has zero automated coverage on the money path. Plan: in `tests/api.rs`, reuse the two-provider fixture (~:3958), POST a Settled webhook to `/v1/zaprite/webhook/{provider-A-id}`, assert only profile A settles (B untouched; an unknown path-id 404s). Existing mock seam, no external account, runs in `cargo test`. Effort S.

## Design (contract conformance)

The brand contract lives in `design/DESIGN.md` + `design/tokens.tokens.json` (distilled
2026-06-16 from the prior Claude Design system, archived in `design/_imports/`). A
`design-checker` audit (2026-06-16) found high fidelity overall. **Adjudicated 2026-06-17:**
the structural palette-consolidation and the token-gap nitpicks were **dropped** — the
consolidation can't actually remove the duplication it targets (the rust-embedded admin SPA
can't `@import` a shared file, so "consolidation" is a verbatim re-copy), and the token-gap
list was partly mis-specified by the audit. Only the three contract-"never" blockers survive.

**Blockers — approved to fix (adjudicated → lean DO; wants an owner glance since they change
public landing + admin visuals). Three reversible CSS one-liners:**
- Gold used as an actionable *fill* (contract: gold is accent/border only, never a fill).
  (a) admin SPA `.featured-pill-toggle.on` → `web/index.html:417-419`; (b) admin sidebar
  upgrade CTA `#tier-banner-cta` → `web/index.html:537`. Fix to navy-fill or gold-border/text.
- Primary buy CTA uses pill radius `999px` (contract: buttons are `r-md` 8px; pill is
  badges-only) — `keysat-xyz-landing/index.html:390`. Set to 8px.
