Skip to main content

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.

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, Canceled events (viem watchContractEvent, 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 / pathReplacement
api/src/payments/wallet.service.ts — HD derivation, Alchemy smart accountFactory computeAddress + Solana findProgramAddressSync
api/src/withdrawals/services/smart-account-wallet.service.ts — drain / transferPaymentLinkReceiver.pay atomic split
api/src/withdrawals/services/solana-withdrawal-transfer.service.ts — SOL sweeppayment_link program pay instruction
api/src/scheduler/processors/subscription-auto-charge.processor.ts — custodial pullSubscriptionHub.charge + Anchor charge
api/src/withdrawals/services/bitcoin-hot-wallet.service.ts — unilateral BTC key2-of-2 multisig per link with merchant signer
hd_wallet_seed table + WALLET_ENCRYPTION_KEYEliminated for EVM/SOL; archived in Phase 7

See also