crypto-processing-api v0.2 — Adoption & Robustness Roadmap¶
Context¶
v0.1.0 is built, tested, published (github.com/OliverD25/crypto-processing-api, GHCR images, CI green). The user's goal now: make it a project other developers adopt — deploy-as-is first (released image next to BTCPay, integrate via API), fork-friendly second. This plan was produced by a 5-agent design workflow (3 lenses reading the real code → synthesis → adversarial critique, verdict "sound after fixes") with all critique amendments folded in. The critique also found two live defects in v0.1 — fixed first as v0.1.1.
Design documents from the workflow output
(C:\Users\Admin\AppData\Local\Temp\claude\E--codespace--claude-code--swift-punk-projects-crypto-processing-api\348375ed-1947-4db3-a5d0-764e772a7f68\tasks\w6x54lpyj.output,
JSON keys contract / robustness / dx / mergedPlan / critique) get salvaged into docs/design/ as files 07–11 at R1 start.
User decisions¶
- Deploy-as-is primary; fork-friendly secondary. Full DX set: docs site, Python + TypeScript SDKs, example app, community kit.
- Extension contract formalized and proven by adding one asset: 2-day Lightning spike first; Litecoin fallback pre-agreed.
- All four robustness tracks (property tests, static analysis, homelab nightly e2e, live Nile) — staged as below.
- Homelab runner: approved (private ops repo + fenced runner on the shared Ubuntu box; check the homelab manual before touching the box).
- Nile session: user committed (~2–3h manual: TronGrid key, faucet, plugin UI). Hard gate before the v0.2.0 tag.
Milestones (build order; two tracks after R3)¶
R1 — v0.1.1 hotfix (live defects; ships alone, immediately)¶
due_for_submission(services/withdrawals.py:835) anddue_for_polling(:845) gainbackend == BACKEND_BTCPAY_PAYOUTfilters (manual-TRON rows are today submitted to BTCPay / polled against Greenfield).- Deadlock prevention (critique 1a):
usdt_auto_withdraw=truebecomes a startupValueErrorinconfig.pyuntil an automated TRON signer exists — an auto-approved manual row is bornAPPROVED, and no code path can ever submit it (admin approve requiresPENDING_APPROVAL). Plus a runbook section: SQL remediation for rows already strandedapproved/submittingon existing deployments. - Job C USDT custody defect (critique 1b):
build_jobs(workers/runner.py:180) never passestron=— USDT insolvency has never been computed by the background job. Wire it (mirror thetron_configuredgate); regression test asserts atrongrid-sourced custody line when TRON is configured. - Regression tests for all three; tag v0.1.1; CHANGELOG; nothing else rides along.
R2 — Robustness guardrails (before touching money code)¶
- Static analysis: CodeQL default setup (repo setting via gh API); Dependabot (security immediate; monthly grouped versions for pip+actions+npm); Semgrep CE pinned,
--error, custom.semgrep/rules —postings-writer(noPostingwrites outsideledger/service.py; must distinguish writes from legit reads likewithdrawals.py:490),no-raw-sql-in-money-path,no-float-amounts+p/python. Skip bandit (ruffSalready on).SECURITY-AUDIT.mdkeyed 1:1 to the threat table in docs/security.md. - Migration robustness: schema round-trip test (upgrade→schema-only normalized dump→downgrade→assert clean→upgrade→identical dump — normalized: sorted, fixed pg version, no volatile values);
alembic check+ single-head CI step; frozen seeded dumptests/fixtures/upgrade/v0.1.0.sql+scripts/make_upgrade_fixture.py+ upgrade test (load →upgrade head→assert_ledger_consistent+ frozen balances). Release checklist: every release adds its dump. - Property-based ledger tests: Hypothesis
RuleBasedStateMachineagainst real Postgres drivingpost_entry+ the withdrawal posting matrix (deposit/hold/submit/settle/unsubmit/release/replay/crash rules). Single-threaded only (critique: thread races in Hypothesis rules are non-reproducible; races stay in the existingtest_ledger_concurrency.py). Profiles:ci(max_examples=15, derandomized),nightly(300, random seed);.hypothesis/artifact on failure. - DoD: a deliberately-introduced rogue
Postingwrite fails semgrep; a deliberately-broken downgrade fails round-trip; nightly-volume property run finds nothing for a week (run locally until R5 exists).
R3 — Asset-extension contract refactor + conformance suite¶
An asset = one DB row (data) + one registry entry (behavior):
- services/asset_registry.py: AssetProfile (fee_policy, destination_validator, withdrawal_backend name, custody_source, payment_method_matcher, has_btcpay_wallet, sweep mode, required flag) + build_registry(settings); startup fails loudly on enabled-asset-without-profile. Explicit in-code registry (mypy-checked) — NOT entry points, NOT DB-stored behavior names.
- Migration 0006: pooled_addresses, invoice_currency, deposit_expiry_minutes columns on assets, server defaults reproducing v0.1.0 exactly (first customer of R2's upgrade tests).
- Protocol split in services/backends.py: AutomatedWithdrawalBackend (initiate/poll_status/cancel/find_for_withdrawal — crash recovery is part of the contract) and OperatorWithdrawalBackend (new_reference/verify_broadcast/confirmations); BackendPayout canonical-state dataclass so _PAYOUT_STATE_MAP stops keying on BTCPay literals; ManualTronBackend finally type-checks.
- Kill the BTCPay leaks (extension design §7): INVOICE_CURRENCY/_expiry_minutes/POOLED_ASSETS dicts → columns; _matches/REQUIRED_ASSETS → registry; validate_destination if/elif → registry; fee/backend routing in api/withdrawals.py, api/admin.py, workers/payout_submitter.py → registry; ONCHAIN_SUFFIX heuristic + _chain_balance USDT special case → has_btcpay_wallet + CustodySource protocol (balance() -> int | None, None ≠ 0).
- Anti-fake-drift (critique 4): recorded-payload corpus — real Greenfield payout/invoice JSON captured from the regtest stack, committed as fixtures, asserted through the normalizer independently of FakeBTCPay. "Unknown payout state → logged no-op" becomes an explicit conformance case.
- Conformance suite inside the package (crypto_processing_api/testing/contracts.py): AutomatedBackendContract, OperatorBackendContract, FeePolicyContract, CustodySourceContract, EndToEndLedgerContract. Both existing backends must pass before R4.
- docs/extending.md (the "add your own asset" guide; honest about what is deliberately fixed: BTCPay as sole deposit rail/webhook source, post_entry, status matrices, BTC anchor).
- DoD: zero if asset.id == routing outside the registry (semgrep-checked); drills 1–7 unchanged; BTC/USDT behavior byte-identical (no wire change); migration 0006 passes both R2 gates.
R4 — Proof asset: 2-day Lightning spike, then build¶
Spike (time-boxed 2 days, regtest): (1) can a BTCPay top-up/LNURL invoice deliver Lightning deposits through apply_invoice_state (what does destination contain, how are payments attributed)? (2) Lightning payout-processor failure/expiry semantics (does AwaitingPayment retry forever?). (3) Can bootstrap_btcpay.py configure it headlessly? → decision memo.
If Lightning: separate BTC_LN asset with own accounts (separate custodial float — stated honestly in ALL positioning); BOLT11 validator; BtcpayPayoutBackend reuse via BTC-LN payout method; LightningNodeCustody; regtest LND nodes + drills 8–11 (instant settle; LN withdrawal; liquidity exhaustion; expired invoice). Scope pulled in from the critique: fee-drift journaling (routing fees booked to network_fee_expense — not deferrable for LN), a SUBMITTED timeout / definitive-failure semantics (backend capability definitive_failure_proof gating narrow auto-release for cryptographically-definitive failures), LN payloads added to the webhook schema (R6).
If Litecoin: LTC via BTCPay chain plugin, regtest litecoind, same conformance-suite proof; deferred-Lightning noted in ROADMAP.
DoD either way: the new backend passes the conformance suite unmodified — that run is R3's acceptance test; docs/extending.md gains the worked example linking real commits.
R5 — Nightly e2e on the homelab (security-critical; may start after R2 with drills 1–7)¶
- Private ops repo
OliverD25/crypto-processing-api-nightlyholds the ONLY runner registration (fork-PR code structurally cannot reach it). Workflow:schedule(03:17) +workflow_dispatchonly; checks out publicmain; concurrency group; 60-min timeout;down -v+ prune inalways(). - Runner on the box (read the homelab-remote-control manual first): ephemeral runner via JIT config (
--jitconfigminted by a host-side script — no PAT in the runner container's env; fine-grained PAT scoped to the ops repo only, held on the host) + sibling dind (production Docker socket never mounted). Fences: dindcpus: 3,mem_limit: 8g,pids_limit, size-bounded dedicated data-root volume + post-run free-space assertion; runnercpus: 1/mem 1g. - Network egress lockdown (critique 2.1): nightly stack on an internal-only bridge; dind egress allows registries + GitHub (+ TronGrid if needed), drops RFC1918 — nightly code can never reach the LAN. Images pinned by digest; pip installs from a hash-locked constraints file.
- Watchdog (critique 2.3): dead-man's-switch workflow in the ops repo on GitHub-hosted runners (plain GITHUB_TOKEN); nightly's last step commits a heartbeat file (keeps both schedules alive past GitHub's 60-day auto-disable); alerts if last green nightly > 26h. Failure alerts via ntfy (random-suffixed topic, ops-repo secret).
- Nightly content: full stack boot → bootstrap → drills 1–11 →
HYPOTHESIS_PROFILE=nightlyintegration suite → example-app loop (once R9 exists) → pip-audit → teardown. Runner-isolation statement in SECURITY-AUDIT.md + homelab manual.
R6 — OpenAPI hardening + community kit + positioning (adoption track; after R3 so the spec is cut once)¶
api/schemas.pyresponse models — amounts and timestamps typedstr(no pydantic re-serialization); round-trip test compares raw bytes of old vs new output (critique 5.2);response_model=+operation_idon all routes; Idempotency-Key + error envelope in OpenAPI;scripts/export_openapi.py→ committeddocs/reference/openapi.json+ CI drift gate.- Webhook payload schemas (critique 5.1): typed models for all 8 outbound event types, JSON Schema exported next to openapi.json, same drift gate — closes the only unmonitored contract surface; R7's
parse_eventgenerates from it. - Community kit: issue forms (bug/feature/operator-report; blank issues off), PR template inlining the 9 money-path invariants, dependabot.yml, CODE_OF_CONDUCT (contact muzexp@gmail.com), ROADMAP.md ("not planned": multi-tenancy, hosted service, non-BTCPay deposit rails, external audit pre-1.0), Discussions categories, ~10 verified good-first-issues,
docs/reference/versioning.md(pre-1.0 semver; Breaking/Migration changelog section mandatory; migrations forward-only, every release upgradeable from previous). - README top-fold rewrite: pitch, who-for/who-NOT-for, honest comparison table (raw BTCPay / hosted processors / build-yourself).
R7 — SDKs (Python → PyPI, TypeScript → npm)¶
- Generated core + thin handwritten facade (~300 lines/language): Python
openapi-python-client(httpx, sync+async, py.typed); TS@hey-api/openapi-ts. - Mandatory handwritten surface: webhook verification (port of
core/signing.py:58-81— raw bytes, 300s window, constant-time) +parse_eventtyped from the R6 JSON Schema; idempotency ergonomics (auto UUID key, same-key retry honoring Retry-After). - Cross-language correctness:
sdks/signature-vectors.jsongenerated by the server's own signer, asserted by server + both SDKs. - Publishing: PyPI trusted publishing (OIDC), npm
--provenancewith one scoped token; idempotent per registry (skip-if-version-exists); one version — the repo tag ships image + both SDKs. Names:crypto-processing-clienton PyPI,@oliverd25/crypto-processing-clienton npm (scoped — bare npm names contested). - User checklist (accounts I cannot create): PyPI account + trusted-publisher registration; npm account + one automation token as a repo secret.
R8 — Docs site¶
MkDocs Material (pinned, zero theme customization), IA: getting-started / integrating / operating / extending / reference / design-record; git mv preserves history; extending.md → extending/adding-an-asset.md; API reference via Scalar over the committed openapi.json — Scalar JS pinned or vendored, never floating CDN (critique 5.3); generated reference/configuration.md from Settings (drift-gated); no versioned docs pre-1.0; docs.yml (--strict on PR, Pages deploy on main).
R9 — Example integration app¶
examples/platform-demo/: single-file FastAPI + Jinja2 + HTMX (~350 lines written as a tutorial) using the Python SDK — fake login, BTC deposit with polling, balances, withdrawal, /platform-webhook implementing the 5-step contract from docs/integrating.md. Opt-in compose profile on the regtest stack. Its full loop runs in the nightly (critique 7: a tutorial that rots fails in front of the worst possible reader).
R10 — Live TRON Nile verification (HARD GATE between R3 and the v0.2.0 tag)¶
Framed as the USDT regression check for the refactor (USDT cannot run on regtest; a matcher regression would silently disable USDT deposits with no CI signal). docs/runbook-nile-verification.md (user's manual steps: TronGrid key, two Nile wallets, faucet, USDt plugin UI per the nile override); scripts/verify_nile.py (preflight: Nile + mainnet contract symbol()/decimals(); guided live deposit; guided live withdrawal with full-tuple verification + 19-block confirmation; duplicate-txid negative check). Afterward: downgrade the format-verified-only caveats with date+txid, docs/verification-log.md, fix tests/fake_tron.py against any real payload differences with captured-payload regressions.
R11 — Release v0.2.0¶
CHANGELOG with Breaking/Migration section; tests/fixtures/upgrade/v0.2.0.sql; one tag ships image + both SDKs; ROADMAP checkboxes closed; Discussions announcement. Upgrade proof: a v0.1.x deployment upgrades by pull + alembic upgrade head + restart (CI-enforced by the frozen-dump test forever).
Backward compatibility (commitment)¶
Migration 0006 additive with defaults reproducing v0.1.0 behavior; no wire-format changes in v0.2 (pinned by raw-bytes round-trip tests); v0.1.0→v0.2.0 upgrade is a permanent CI invariant via the frozen dump.
Repo-settings actions (done via gh where the API allows; else listed for the user)¶
Enable: CodeQL default setup, Dependabot, Discussions, Pages. Create: private ops repo (approved). File: good-first-issues (R6). User-only: PyPI/npm accounts + publisher registration (R7 gate), Nile manual session (R10), homelab SSH session for the runner install (R5).
Verification¶
- Every milestone: full local suite (569+ tests) + ruff + mypy --strict green; drills 1–7 (later 1–11) green on regtest; independent verification by the orchestrator after each coder-agent report.
- R2 gates prove themselves on R3's migration 0006. R3's acceptance = conformance suite passing for both existing backends + byte-identical wire behavior. R4's acceptance = new backend passes the suite unmodified. R5 DoD: three consecutive green nightlies + simulated failure alert + simulated silence trips the watchdog. R10 = live Nile deposit + withdrawal logged. R11 = staged v0.1.x→v0.2.0 upgrade pass.
Process¶
Implementation milestone-by-milestone via coder agents (self-contained prompts; same agent continued per milestone), incremental commits, orchestrator verifies between milestones. Design docs salvaged to docs/design/07-11 at R1. Nothing destructive without asking; no history rewriting; the homelab manual is read before any change on the box.