LPsLux Proposals
Research
LP-3040

Banking Service Interface

Draft

The provider-neutral BaaS and CaaS API surface — accounts, payments, cards, crypto, FX, and membership plans under /v1/bank.

Category
Interface
Created
2026-08-21

LP-3040: Banking Service Interface

Abstract

This LP standardizes the banking service interface served by bankd (luxfi/bank): one authenticated REST surface under /v1/bank covering accounts, balances, transfers, outbound payments, beneficiaries, cards, crypto movement, exchange, FX, and membership plans. The interface is provider-neutral by construction — the banking counterparty (issuer), FX venue, and crypto backend are deployment choices behind stable route contracts, never API differences. A single deployment switch (BANK_SANDBOX) selects the seeded testnet sandbox or live rails; the surface is identical in both. Clients integrate once — dashboard, white-label tenant, SDK, or agent — and inherit every backend.

Motivation

Banking APIs rot when upstream vendors leak into the client contract. Every issuer, card processor, and exchange has its own lifecycle, error dialect, and identifier scheme; a surface that exposes them forces every client to re-integrate on every counterparty change. The interface in this LP absorbs that variance at exactly one seam per concern and publishes one shape.

Specification

Authentication and environments

Every route below requires Authorization: Bearer <JWT> issued by Lux IAM (lux.id), except those marked public. Record ownership is enforced server-side by matching the authenticated principal against the owner of every account-scoped record.

EnvironmentAppAPI
Productionapp hosthttps://api.<domain>/v1
Sandboxsandbox hosthttps://api.sandbox.<domain>/v1

Sandbox and production run the same binary; BANK_SANDBOX (default on) selects deterministic settlement, seeded fixtures, testnet crypto, and the simulated issuer.

Route catalog

MethodPathPurposeAuth
GET/v1/bank/healthHealth + sandbox flagpublic
GET/v1/bank/configCurrencies, network, partner disclosurepublic
GET/v1/bank/plansMembership ladder (see below)public
POST/v1/bank/onboardCreate account from KYC profileJWT
GET/v1/bank/overviewDashboard aggregateJWT
GET/v1/bank/transactionsTransaction streamJWT
GET/v1/bank/account/summaryAccount listJWT
POST/v1/bank/transfersBook transfer between own accountsJWT
POST/v1/bank/payments/outboundPayment to a beneficiary; rail selected downstreamJWT
GET/v1/bank/accounts/{id}/balancesPer-currency balances (available + held, minor units)JWT
GET/v1/bank/accounts/{id}/walletsOn-chain walletsJWT
GET/v1/bank/accounts/{id}/transactionsAccount historyJWT
GET/POST/DELETE/v1/bank/beneficiaries[/{id}]Counterparty registryJWT
GET/POST/v1/bank/cardsCard ledger; issueJWT
POST/v1/bank/cards/{id}/freeze · /unfreezeCard stateJWT
POST/v1/bank/cards/accountOpen issuer card accountJWT
GET/v1/bank/cards/accountIssuer account stateJWT
GET/v1/bank/cards/kycIssuer KYC statusJWT
POST/v1/bank/cards/virtualCreate virtual-card accountJWT
GET/v1/bank/cards/virtual/kyc-urlHosted KYC URL (sensitive; never log)JWT
GET/v1/bank/cards/virtual/consent-urlConsent URL (sensitive; never log)JWT
POST/v1/bank/cards/virtual/orderOrder the card once approvedJWT
POST/v1/bank/crypto/sendOn-chain send from the walletJWT
POST/v1/bank/crypto/depositTestnet faucet (sandbox only)JWT
GET/v1/bank/crypto/pricesReference pricesJWT
GET/v1/bank/walletWallet + crypto holdingsJWT
POST/v1/bank/exchange/quote · /executeFiat FX + crypto buy/sell/convertJWT
POST/v1/bank/fx/quote · /executeDealt FX (forexd)JWT
POST/v1/bank/webhooks/*Inbound provider events, HMAC-SHA256 signedHMAC

Amounts are integers in minor units; decimals in responses tells the client how to render (2 for most fiat, 0 for JPY, 6 for crypto).

The issuer seam (CaaS)

Card issuance runs the issuer's own account, KYC, and consent lifecycle. Clients branch only on the normalized status + nextAction pair:

statusnextActionClient action
not_startedregisterPOST /v1/bank/cards/virtual
pendingcomplete_kycfetch KYC URL, open in browser, poll
pendingaccept_agreementfetch consent URL, open, poll
pendingnonewait, poll
rejectedretry_kycfresh KYC URL, retry, poll
approvedorder_cardPOST /v1/bank/cards/virtual/order
errorretry_kycretry; support if persistent

Platform KYC and issuer KYC never substitute for each other. The issuer implementation (Issuer in luxfi/bank issuer.go) is selected by BANK_ISSUER; the sandbox simulates it deterministically. Hosted URLs pass straight to the user's browser and are never logged or persisted.

The FX and crypto seams

Dealt FX executes through forexd provider adapters (FXProvider in luxfi/forex); crypto conversion deals on a spot venue adapter and settles plain fiat over banking rails. The crypto movement routes (send, sandbox deposit) operate on the account wallet; in sandbox the ledger is the testnet backend and every send returns a txHash on lux-testnet.

Membership plans

GET /v1/bank/plans publishes the plan ladder — id, name, monthly price, included card and IBAN, rail allowances, FX and deposit rates, transaction limits, holders. A plan on the account overrides entity-type transaction limits. The published ladder is the single pricing source for every surface that renders plans.

Status transitions

pending -> processing -> completed
pending -> failed | cancelled
processing -> failed | cancelled

All other transitions are rejected. Debits hold funds atomically at creation; completion releases the hold; failure and cancellation reverse it.

Rationale

One seam per concern (Issuer, FXProvider, crypto backend) keeps verification logic and policy enforcement unbraided: the route contract states what the product does; the adapter states how one counterparty does it. The status/nextAction pair is the smallest interface that survives every issuer's KYC workflow without leaking it. Minor-unit integers plus a decimals hint eliminate float drift across 2/0/6-dp currencies.

Backwards Compatibility

New interface; no prior standard. Additive response fields (e.g. an issuer's virtualAccount object) MUST be tolerated when absent.

Security Considerations

IAM is the only authorizer; there is no API-key path on customer routes. Hosted KYC/consent URLs are sensitive and never logged, persisted, or sent to analytics. Webhooks validate X-Signature = hex(HMAC-SHA256(secret, body)). The audit log is immutable (update/delete blocked at the hook layer). Balance debits are transactional; the floor check rejects any write that would take available below zero.

Copyright and related rights waived via CC0.