Architecture
This page describes the target architecture of the non-custodial OrcaRail stack and, for each component, the legacy code path it replaces.
System diagram
Components
PaymentLinkFactory (EVM)
One per chain. Deterministic CREATE2 addresses derived from a 32-byte hash of the payment-link UUID. Exposes:
computeAddress(bytes32 linkId) view returns (address)— pre-deployment address for the UI and API.deployAndPay(bytes32 linkId, Split split, address token, uint256 amount)— lazy deploy + forward in one transaction.isDeployed(bytes32 linkId) view returns (bool).
Upgradeable behind a UUPS proxy owned by the 3-of-5 Safe multisig; 48-hour timelock. Per-link receivers are not upgradeable.
Replaces: the EVM branch of generateAddressForNetwork in api/src/payments/wallet.service.ts (HD derivation + Alchemy counterfactual smart-account address computation).
PaymentLinkReceiver (EVM)
EIP-1167 minimal clone, one per payment link. Accepts native and ERC-20 (SafeERC20 for non-standard tokens like USDT). Emits Paid(linkId, payer, token, amount). Calls FeeSplitter.split() atomically.
Replaces: the sweep step in SmartAccountWalletService.drainSmartAccountWallet in api/src/withdrawals/services/smart-account-wallet.service.ts.
FeeSplitter (EVM)
Stateless split logic. Takes a Split struct in basis points and an amount; distributes to merchant, platform, referral, and bridge-fee addresses. Remainder goes to the merchant (no dust).
Replaces: the TypeScript split math in drainSmartAccountWallet and buildNativeDrainCalls / equivalent ERC-20 branch of the same service.
SubscriptionHub (EVM)
One per chain. Stores a Subscription struct per bytes32 id. charge(id) is public and permissionless, enforcing:
block.timestamp >= nextChargeAt- monotonic
lastChargedAt amountCharged + amount <= cap- ERC-20 allowance + balance checks
Emits Charged, ChargeSkipped(reason), Canceled, SubscriptionCreated.
Replaces: the custodial pull in api/src/scheduler/processors/subscription-auto-charge.processor.ts that today uses an OrcaRail-controlled smart account.
payment_link program (Solana)
Anchor program with a PDA per link derived from [b"pl", link_id]. Instructions: initialize, pay, refund. Splits via CPI.
Replaces: the Solana branch of generateAddressForNetwork in api/src/payments/wallet.service.ts (ed25519-hd-key derivation) and the SOL sweep path in api/src/withdrawals/services/solana-withdrawal-transfer.service.ts.
subscription program (Solana)
Anchor program using SPL Approve / Revoke as the delegate-authority primitive. charge(id) mirrors EVM SubscriptionHub.charge(id) semantics.
Replaces: the SOL branch of subscription-auto-charge.processor.ts and the custodial pull via OrcaRail-controlled token accounts.
BTC 2-of-2 multisig + pre-signed refund
Per-link taproot address with a merchant pubkey and a platform pubkey. Pre-signed refund PSBT stored off-chain at deposit time. Platform cannot unilaterally move funds.
Replaces: the unilateral hot wallet in api/src/withdrawals/services/bitcoin-hot-wallet.service.ts. The sweep pathway in api/src/withdrawals/services/bitcoin-withdrawal-transfer.service.ts is retained but signs via multisig instead of a unilateral key.
Event indexer
A new service, payments/indexer/*, subscribes to:
- EVM
Paid,SubscriptionCreated,Charged,ChargeSkipped,Canceledevents (viemwatchContractEvent, Alchemy primary + public RPC fallback). - Solana program logs (Helius webhooks or Triton streams).
It upserts PaymentTransaction and SubscriptionAutoCharge rows and dispatches the existing webhook events in api/src/webhooks/webhooks.service.ts.
Replaces: the deposit-balance polling and sweep-completion callbacks that today kick webhooks.
Data flows
One-off payment (EVM)
Subscription charge (EVM)
Refund
EVM: merchant keeper EOA or multisig calls PaymentLinkReceiver.refund(txId). Solana: signer invokes the refund instruction. BTC: merchant broadcasts the pre-signed refund PSBT.
Cross-chain settlement
Relay is still used (see api/src/withdrawals/services/bridge), but the trigger is on-chain instead of custodial: the receiver calls the Relay executor in the same transaction, or the keeper does it immediately on the Paid event.
What this replaces in the codebase
A condensed list for engineers. Replaced or bypassed in phases 1–4; deleted in Phase 7.
| Legacy file / path | Replacement |
|---|---|
api/src/payments/wallet.service.ts — HD derivation, Alchemy smart account | Factory computeAddress + Solana findProgramAddressSync |
api/src/withdrawals/services/smart-account-wallet.service.ts — drain / transfer | PaymentLinkReceiver.pay atomic split |
api/src/withdrawals/services/solana-withdrawal-transfer.service.ts — SOL sweep | payment_link program pay instruction |
api/src/scheduler/processors/subscription-auto-charge.processor.ts — custodial pull | SubscriptionHub.charge + Anchor charge |
api/src/withdrawals/services/bitcoin-hot-wallet.service.ts — unilateral BTC key | 2-of-2 multisig per link with merchant signer |
hd_wallet_seed table + WALLET_ENCRYPTION_KEY | Eliminated for EVM/SOL; archived in Phase 7 |