Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ update the existing canonical document when one owns the topic.
`docs/adr-NNNN-<topic>.md`, with current behavior updated in the relevant maintained
document.
- Keep local `README.md` files only when they explain a non-obvious subsystem contract.
- Never create or extend a document whose audience includes a client or partner, or that
tracks commercial gates, terms or negotiation status: it belongs in the private
`pendulum-chain/vortex-private` repository (see "Public Repository: No Client Data").
- Never add a document written for a specific client or partner, or one that tracks
commercial gates, terms or negotiation status (see "Public Repository: No Client Data").
- Operator runbooks are maintained outside this repository. Cite them by name and section
(for example "runbook §2.7"); when a change alters a documented procedure, say so in the
pull request description.
- Repair indexes and relative links in the same change as a move or deletion.
- Agent skills live in `.agents/skills/<name>/SKILL.md` so every coding agent can use
them; `.claude/skills/` holds only symlinks to those directories (Claude Code follows
Expand Down
2 changes: 1 addition & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ BROWSER_SDK_ORIGINS=
# Environment Configuration
SANDBOX_ENABLED=false

# Sales-demo account (sandbox only) — see docs/operations-demo-environment.md
# Sales-demo account (sandbox only) — see the internal demo-environment runbook
DEMO_ACCOUNT_EMAIL=demo@satoshipay.io
# Serves Alfredpay KYB from canned in-process responses so the demo corridor can be
# re-onboarded endlessly. Refuses to start unless DEPLOYMENT_ENV=sandbox.
Expand Down
2 changes: 1 addition & 1 deletion apps/api/scripts/monerium-b2b-sandbox-e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* End-to-end run of the Monerium B2B onramp against a live backend, the sandbox or a local
* rehearsal (docs/operations-monerium-b2b-runbook.md §8). It registers a client's destination
* rehearsal (B2B operations runbook §8). It registers a client's destination
* the way the partner does, waits until the keeper has deployed, linked and activated the
* account, then waits for one EUR payment and checks on chain that it was converted and
* forwarded, or refunded with --refund.
Expand Down
2 changes: 1 addition & 1 deletion apps/api/scripts/seed-demo-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* Sandbox only — the restore routine itself refuses to run anywhere else. The demo profile
* must have signed in once via OTP first; the seed cannot forge a Supabase Auth user.
* See docs/operations-demo-environment.md.
* See the internal demo-environment runbook.
*/
import path from "node:path";
import dotenv from "dotenv";
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api/services/demo/demo-account.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* carries one of these ids, so restore is idempotent and can never touch a real ramp,
* recipient, or provider record created during a demo.
*
* See docs/operations-demo-environment.md.
* See the internal demo-environment runbook.
*/

/** All demo-owned primary keys share this prefix. */
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/api/services/demo/demo-account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface DemoRestoreSummary {
* rows. Every write is keyed by a fixed demo id, so running it twice is the same as
* running it once and it can never overwrite a real ramp created during a demo.
*
* Sandbox only. See docs/operations-demo-environment.md.
* Sandbox only. See the internal demo-environment runbook.
*/
export async function restoreDemoAccount(): Promise<DemoRestoreSummary> {
if (config.deploymentEnv !== "sandbox") {
Expand Down
6 changes: 2 additions & 4 deletions apps/api/src/api/services/monerium-b2b/monitoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,7 @@ export async function runExecutableDepthCheck(): Promise<void> {

const verdict = classifyExecutableDepth(best.minImpactBps, best.capImpactBps, slippageBps);
if (verdict.severity === "error") {
logger.error(
`monerium-b2b: DEPTH BELOW FLOOR — ${verdict.reason}; triage per docs/operations-monerium-b2b-runbook.md §3. ${detail}`
);
logger.error(`monerium-b2b: DEPTH BELOW FLOOR — ${verdict.reason}; triage per the B2B operations runbook §3. ${detail}`);
} else if (verdict.severity === "warn") {
logger.warn(`monerium-b2b: ${verdict.reason}. ${detail}`);
} else {
Expand Down Expand Up @@ -417,7 +415,7 @@ export async function runStrandedBalanceMonitor(now: number = Date.now()): Promi
* Association monitor (S1 detective control): compares the Monerium-side linked
* addresses + IBAN state per active account against the DB record and alerts on ANY
* change. Error-level: an unexplained association change is an incident trigger
* (docs/operations-monerium-b2b-runbook.md).
* (B2B operations runbook).
*/
export async function runAssociationMonitor(): Promise<void> {
const accounts = await monitoredAccounts([MoneriumAccountStatus.Active]);
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/api/services/phases/blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ apps/api/src/api/services/phases/blocks/
avenia-{direct-mint,mint,moonbeam-mint}/ # BRL onramp variants
avenia-{offramp-fee,offramp-payout,pendulum-offramp}/
mykobo-{mint,offramp-fee,offramp-payout}/ # legacy EUR recovery phases
monerium-{issue,self-transfer}/ # active EUR issue/custody phases
monerium-{issue,self-transfer}/ # active EUR issue/self-transfer phases
uniswap-v3-fixed-swap/ # pinned Polygon EURe/USDC conversion
{evm,assethub}-offramp-source/ # source validation and tx plans
fund-ephemeral/ # EVM/Substrate funding and source-hash checks
Expand Down Expand Up @@ -607,7 +607,7 @@ Unmapped cases fail at quote resolution; there is no alternate engine:
the persisted subsidy. Provider orders remain bound to those persisted terms; if
an expired order cannot be replaced
without degrading them, execution pauses before the single-use provider transfer
and leaves the funds on the client-custodied Polygon ephemeral for reconciliation.
and leaves the funds on the client-held Polygon ephemeral for reconciliation.

### Runtime ownership

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/config/vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function readRecipientInviteDiscountLimit(): number {
interface Config {
env: string;
deploymentEnv: DeploymentEnv;
/** Login email of the seeded sales-demo account. Sandbox only; see docs/operations-demo-environment.md. */
/** Login email of the seeded sales-demo account. Sandbox only; see the internal demo-environment runbook. */
demoAccountEmail: string;
/**
* Replaces the Alfredpay client with a canned in-process stand-in so the demo corridor can be
Expand Down
13 changes: 5 additions & 8 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ The smaller set of general project documents stays directly in `docs/`:
| [`architecture-email-notifications.md`](architecture-email-notifications.md) | Current transactional/auth email architecture: queue, dispatch, producers |
| [`architecture-identity-model.md`](architecture-identity-model.md) | Current cross-module identity and ownership architecture |
| [`architecture-monerium-b2b-onramp.md`](architecture-monerium-b2b-onramp.md) | Current end-to-end architecture of the B2B EUR onramp: onboarding, deposit-to-payout, batching, fees, data model |
| [`operations-brl-tax-id-claim-release.md`](operations-brl-tax-id-claim-release.md) | Operator runbook for releasing a BRL tax ID squatted through `createSubaccount` (RISK-026) |
| [`operations-demo-environment.md`](operations-demo-environment.md) | Setup and runbook for the sandbox sales-demo account |
| [`operations-legacy-schema-cleanup.md`](operations-legacy-schema-cleanup.md) | Deployment gates and recovery runbook for irreversible migrations 060-061 |
| [`operations-monerium-b2b-rollout.md`](operations-monerium-b2b-rollout.md) | Deploy checklist, launch readiness, and open items ledger for the B2B onramp |
| [`operations-monerium-b2b-runbook.md`](operations-monerium-b2b-runbook.md) | Operator procedures for the B2B onramp: onboarding, incidents, alert triage, dormancy, migration |
| [`operations-monerium-interface.md`](operations-monerium-interface.md) | Monerium onramp boundary across the OAuth and white-label apps plus the profile, address, IBAN, and payment interface reference |
| [`operations-testing.md`](operations-testing.md) | Maintained test strategy and suite boundaries |
| [`product-dashboard.md`](product-dashboard.md) | Current dashboard product scope and acknowledged gaps |
Expand Down Expand Up @@ -88,11 +83,13 @@ Do not add:
- completed implementation plans or refactor summaries;
- a second architecture document for behavior already owned by `security-spec/`;
- an in-repository archive of stale docs. Git history is the archive;
- operator runbooks (incident, rollout, environment and migration procedures). They are
maintained outside this repository; cite them by name and section (for example
"runbook §2.7");
- anything that names clients, partners or prospects, or records agreement or negotiation
status, terms agreed with a specific party, onboarding status or contacts. This
repository is public: documents written for a partner and rollout trackers with
commercial gates live in the private `pendulum-chain/vortex-private` repository. Docs
here describe mechanisms with neutral roles ("the partner") and present parameters as
repository is public: documents written for a specific partner and rollout trackers
with commercial gates do not belong here. Docs here describe mechanisms with neutral roles ("the partner") and present parameters as
configurable defaults; they may cite a registry ID, never the deal behind it.

### Proposals and decisions
Expand Down
4 changes: 2 additions & 2 deletions docs/adr-0004-sandbox-demo-environment.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ADR 0004: Sandbox Sales-Demo Account

Status: accepted 2026-08-07. Implemented in `apps/api/src/api/services/demo/`; the runbook
is [`operations-demo-environment.md`](operations-demo-environment.md).
Status: accepted 2026-08-07. Implemented in `apps/api/src/api/services/demo/`; operating it
follows the internal demo-environment runbook.

## Context

Expand Down
21 changes: 10 additions & 11 deletions docs/adr-0005-monerium-b2b-onramp.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ acceptances* of the B2B EUR → USDC onramp. How the system works lives in
[`architecture-monerium-b2b-onramp.md`](architecture-monerium-b2b-onramp.md); security
invariants and the threat model in
[`security-spec/05-integrations/monerium-b2b.md`](security-spec/05-integrations/monerium-b2b.md);
launch gates and the deploy checklist in
[`operations-monerium-b2b-rollout.md`](operations-monerium-b2b-rollout.md); procedures in
[`operations-monerium-b2b-runbook.md`](operations-monerium-b2b-runbook.md). The
launch gates, the deploy checklist and operator procedures in the internal operations
runbooks. The
consumer-flow design this grew out of remains a phase-2 proposal:
[`proposal-monerium-consumer-onramp.md`](proposal-monerium-consumer-onramp.md).

Expand All @@ -38,7 +37,7 @@ of access to funds.
**Why the naive alternative is unsafe:** Monerium validates *redeem orders* ("Send EUR
`<amount>` to `<IBAN>` …") through the same EIP-1271 interface. A general-purpose
validation key would let its holder redeem the client's EURe to an arbitrary IBAN —
a fiat theft path and unambiguous custody. The whole design follows from closing it.
a fiat theft path. The whole design follows from closing it.

Supporting decisions, all in force:

Expand All @@ -52,11 +51,11 @@ Supporting decisions, all in force:
treasury.
- **No upgradeability, ever.** Immutable-and-migratable: evolution (new tokens, a new
router, contract fixes) happens by deploying a new implementation + factory and
migrating clients clone-by-clone — never by mutating deployed code. The custody
argument depends on it. Routes, the fee policy and the vault limits are bounded
migrating clients clone-by-clone — never by mutating deployed code. The design's
guarantees depend on it. Routes, the fee policy and the vault limits are bounded
*data* the guardian may change within immutable validation, not code.
- **No client key on the clone** (amended 2026-09-17; superseded the mandatory
self-custodied `fallbackAddress` of 2026-07-17 and its `sweep`/config functions and
client-controlled `fallbackAddress` of 2026-07-17 and its `sweep`/config functions and
dead-man sweep). The only exits are the client's fixed `destination` and, for a payment
the promised window was missed on, the client's refund wallet held by Vortex — see
the amendments of 2026-09-17 and 2026-10-01. A destination change means a new clone (runbook §5).
Expand Down Expand Up @@ -315,7 +314,7 @@ the promised window. Decisions:
| T4 | Sandbox wire-format verifications | Webhook digest encoding, delivery id field, order-state vocabulary, and the EIP-191 link-hash variant were confirmed against the sandbox during G0; re-verify against production before first mainnet deposit |
| T1 | Issuer recovery message | **Identical to the link message** — already whitelisted, recovery works as built; `RECOVERY_HASH` stays 0 |
| O1 | Client-migration tooling | Build when first needed; manual procedure in the runbook meanwhile |
| O2 | `FEE_RECIPIENT` treasury | **New dedicated Safe multisig** (immutable at implementation deploy); guardian key to hardware/multisig custody at GA |
| O2 | `FEE_RECIPIENT` treasury | **New dedicated Safe multisig** (immutable at implementation deploy); guardian key moves to hardware/multisig at GA |

## Review history (details in git history)

Expand Down Expand Up @@ -352,8 +351,8 @@ example (oversized-deposit allocation).
dormancy gate turns a rotation after 60 idle days into a pause, and an optional penny
test catches a wrong address when run; a rotation on an account that keeps converting
is not detected, and its forwards keep going to the old address.
- **Vortex custody on the refund path** (amendment 2026-09-17). A recovered payment
sits in Vortex's own wallet until the bank refund goes out; a compromised keeper plus
- **Refund path** (amendment 2026-09-17). A recovered payment waits on the client's
refund wallet until the bank refund goes out; a compromised keeper plus
recovery key could divert a payment the window was missed on. Bounded by the immutable
wallet, the on-chain delay, explicit amounts, a dedicated linked address holding
nothing else, and the association monitor.
Expand Down Expand Up @@ -388,7 +387,7 @@ example (oversized-deposit allocation).
Zero-touch onboarding works end to end (validated against the Monerium sandbox: link
accepted, IBAN issued, no client interaction). A Vortex outage can never trap converted
funds (the permissionless path), and a payment the promised window was missed on is
refunded rather than parked, at the price of Vortex custody on that path. The cost: every rescue path must be designed in upfront
refunded rather than parked. The cost: every rescue path must be designed in upfront
(no universal owner key), fee-policy increases are timelocked and venue changes are
bounded by on-chain route validation rather than admin switches, the client's rate
guarantee is enforced by the contract at the cost of a treasury-funded subsidy budget,
Expand Down
8 changes: 4 additions & 4 deletions docs/adr-0007-monerium-b2b-partner-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
invariants in
[`security-spec/05-integrations/monerium-b2b.md`](security-spec/05-integrations/monerium-b2b.md)
(0, 12, 15 and the key separation), procedures in
[`operations-monerium-b2b-runbook.md`](operations-monerium-b2b-runbook.md) §1, §6 and §8.
the internal B2B operations runbook, §1, §6 and §8.

## Context

The partner creates its clients' profiles and submits their KYB in its own Monerium
white-label app, so it holds each new client's Monerium profile ID; only it knows the
client's payout wallet. Until now a Vortex operator deployed every forwarder with `cast`
and mapped the account with an admin call, so every client waited on a manual step, and
the guardian key, which ADR-0005 intends to move to cold custody, was the only key that
the guardian key, which ADR-0005 intends to move to cold storage, was the only key that
could deploy.

## Decision
Expand Down Expand Up @@ -73,7 +73,7 @@ could deploy.
it sends with implicit nonces like the keeper and the float wallet. Added before any
Sepolia or mainnet factory exists, because the factory is not upgradeable. This does
not make the guardian key cold: the backend still requires it at boot and signs the
dormancy pause (`setGuardianPaused`) with it, so that pause is the open custody item
dormancy pause (`setGuardianPaused`) with it, so that pause is the open guardian-key item
of ADR-0005 O2 (a pause-only role, or an operator-run pause, before a cold guardian).
The guardian can also revoke a clone: `revokeForwarder` is one-way and removes it from
the registry (below).
Expand Down Expand Up @@ -112,7 +112,7 @@ could deploy.

Alternatives rejected: a `requested` status inside `monerium_accounts` (would weaken the
invariant every account row relies on); an operator deploying from a registration queue
(keeps the manual step); deploying with the guardian or keeper key (custody goal, nonce
(keeps the manual step); deploying with the guardian or keeper key (cold-key goal, nonce
interference); merging registrations into the account list (would change a typed
response partners already read).

Expand Down
2 changes: 1 addition & 1 deletion docs/api/pages/14-managed-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ X-API-Key: sk_test_...
```

4. Wait for an `ACCOUNT_UPDATED` event whose `payload.status` is `"active"` and whose `payload.iban` is set. In sandbox, an account activates on its own once its IBAN is issued. The event's `payload.profileId` is the client's managed profile: send it as `X-Managed-Profile-Id` in the reads below. `GET /v1/monerium-b2b/accounts` with your manager key lists the same accounts if you missed an event.
5. Ask Vortex to send a test payment of at least EUR 1 to the client's IBAN. Currently, test payments run in a joint session with Vortex: a normal one, one large enough to convert in several chunks, and one that is refunded. `DEPOSIT_UPDATED` events follow each payment until the deposit is `forwarded` to the destination, or `refunded` for the refunded one.
5. Test payments are not available yet: sandbox cannot currently send EUR to a client's IBAN, and Vortex will tell you once it can. When a payment arrives, `DEPOSIT_UPDATED` events follow it until the deposit is `forwarded` to the destination, or `refunded` if it is not converted within the refund window.
6. Read the client's account and deposits. They return `{ "account": { ... } }` and `{ "deposits": [ ... ], "pagination": { "limit", "offset", "total" } }`, each deposit being the same snapshot `DEPOSIT_UPDATED` delivers. A managed child without a business EUR account returns `404 MONERIUM_B2B_ACCOUNT_NOT_FOUND`; an ID that is not your child returns `403 MANAGED_PROFILE_ACCESS_DENIED`.

```http
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture-identity-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ quote cannot be claimed by another user.
- Provider ownership resolution: `apps/api/src/api/services/avenia-account.ts` and provider controllers/services
- Schema history: `apps/api/src/database/migrations/038-*` onward
- Managed-profile schema: `apps/api/src/database/migrations/063-create-managed-profiles.ts`
- Migrations 060-061 production gates: [`operations-legacy-schema-cleanup.md`](operations-legacy-schema-cleanup.md)
- Migrations 060-061 production gates: the internal legacy-schema-cleanup runbook
- Security details: `docs/security-spec/01-auth/`, `03-ramp-engine/recipient-transfers.md`, and the provider specs under `05-integrations/`

Update this document only when the cross-module shape changes. Provider-specific flows,
Expand Down
3 changes: 1 addition & 2 deletions docs/architecture-monerium-b2b-onramp.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ clients. Normative security detail lives in
[`security-spec/05-integrations/monerium-b2b.md`](security-spec/05-integrations/monerium-b2b.md);
decisions, final parameters, and accepted risks in
[`adr-0005-monerium-b2b-onramp.md`](adr-0005-monerium-b2b-onramp.md);
launch gates in [`operations-monerium-b2b-rollout.md`](operations-monerium-b2b-rollout.md);
operator procedures in [`operations-monerium-b2b-runbook.md`](operations-monerium-b2b-runbook.md).
launch gates and operator procedures in the internal operations runbooks.

## The shape in one paragraph

Expand Down
Loading
Loading