Skip to main content

Custody model

The single most consequential architectural decision. Read this before touching anything that moves money.

Model A — Infinia holds user fiat balances

When a user deposits £100 via FPS, that £100 lives in their Infinia GBP virtual account. GrinGo never sees it, never holds it, never has a key that could move it.

Custody sits at Infinia. GrinGo is a stateless orchestrator:

  • We call POST /v1/accounts/internal-transfer/quote/ to price an FX move.
  • We call POST /v1/accounts/internal-transfer/ to execute the FX.
  • We call POST /v2/payouts to push funds out.
  • We record what happened in our own ledger.
  • We never hold a signing key that could authorise any of these.

Why we picked Model A over a wallet-based alternative

The obvious alternative — call it Model B — would keep the user's balance as USDC on Base in a non-custodial Privy embedded wallet, and use Infinia only as an on/off-ramp. That's how the earlier Bridge-based architecture worked.

DimensionModel AModel B
PoS latency~15–20s (single Infinia hop)~35s (on-chain leg + Infinia hop)
FX events per payment1 (GBP → BRL)2 (GBP → USDC on deposit, USDC → BRL on payment)
On-chain complexityNoneviem, RPC, USDC contract reads, paymaster for sponsored gas
Vendor count1 (Infinia) + auth + KYC bundled in Infinia HOSTED2 (Infinia + wallet vendor) + on-chain infra
CustodyInfinia (regulated third party)User (self-custody via MPC or embedded wallet)
Vendor lock-inHigh — swapping vendors means migrating customer moneyLow — vendor is just a rail
Regulatory postureGrinGo as agent/introducer on top of a custodial partnerGrinGo as non-custodial software

For a tourist payments app, users care about speed and reliability, not self-custody. Model A wins for MVP on the criteria that actually drive product adoption. Model B's advantages are real but only pay off in scenarios (crypto-native audience, cross-chain features, DeFi integrations) that aren't part of the MVP roadmap.

Model A depends on Infinia being licensed to hold consumer GBP/EUR balances for our target users. That is a prerequisite to confirm before build, not an assumption to live with.

The migration path back to Model B

Model A creates real vendor lock-in. Swapping Infinia for another custodial partner means moving customer money — a 4–6 week engineering effort plus regulatory ceremony. That's normal fintech (Wise, Revolut, Monzo all deal with it when partner banks change), but it's not free.

The nuclear escape hatch — if Infinia's service, pricing, or licensing ever turns bad — isn't "swap to another custodial vendor" but "shift to Model B". Once user balances live in user-controlled wallets, the payments provider is just a rail and becomes trivially swappable.

Realistic total effort: 2–3 months of concentrated engineering + a gradual per-user rollout.

One-way door. Going Model B → back to Model A would mean asking users to hand custody back, which is effectively irreversible in practice. That's fine — the point of migrating is specifically to shed custodial risk.

User-visible impact after migration:

  • New "your wallet" UI (address, USDC balance)
  • On-chain deposits (funding via bank → Infinia → USDC → wallet)
  • Slower PIX (~35s vs ~20s — on-chain leg is added)
  • Recovery / seed-phrase concerns (or MPC social recovery, depending on wallet vendor)
  • GrinGo's regulatory posture shifts from "orchestrator on top of custodial partner" to "non-custodial software provider"

Design constraints to keep this door open

These cost ~1–2 days spread across the initial build and cut a future migration from "8+ weeks" to "manageable". Every contributor should recognise them.

ConstraintWhy it matters
users.id is our internal UUID, never derived from an Infinia identifierUsers survive an Infinia disappearance.
(auth_provider, auth_provider_user_id) is stored and treated as stableWeb3Auth-style MPC wallets derive addresses deterministically from this. Same user → same wallet address forever, no migration UX.
Balance is computable from the transactions ledger aloneReconciliation and vendor-swap don't need to interrogate Infinia to know how much a user has.
KYC verified identity stored server-side, not just referenced by Infinia's owner idIf we ever swap payments vendor, we can replay identity to the new vendor without re-onboarding users. Trade-off: increases GDPR exposure surface — encrypted at rest via pgcrypto; see privacy for the middle-ground option (minimal fields cached, rest fetched on demand).
PaymentProvider interface — route handlers never import the Infinia client directlyAdding a wallet-based rail or a second fiat vendor doesn't require route rewrites.
Normalised ProviderEvent webhook shape — Infinia-specific fields translated at the edgeNew vendor = new webhook parser, everything downstream unchanged.
Normalised transaction status enum (pending / settling / completed / failed), not Infinia's PROCESSING / SETTLING / COMPLETED / …Vendor-specific vocabulary doesn't leak into the ledger.
No infinia_* column names outside provider_accounts.provider_account_idRenames become data migrations, not schema redesigns.

See payment-providers for how the PaymentProvider interface is structured in code.