Architecture

                    ┌── connect wallet ──┐
Customer ──> checkout                    │  one payable tx
                    └── send to address ─┘
                              │          │
                              ▼          ▼
                CREATE2 deposit    SpecieRouter.pay(paymentId, merchant)
                     address              │
                              │          │  atomic split, holds nothing
                    (relayer settles,    │
                     permissionless)     │
                              ▼          ▼
                    SpecieFactory ──┤
                              │          │
                    ┌─────────┴──────────┴────┐
                    ▼                         ▼
              merchant address          fee collector
                              │
                              │  PaymentReceived / PaymentSettled
                              ▼
                        chain watcher
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
              payments table     webhook dispatcher ──> your endpoint
                                                    └─> Telegram / Discord

Why contracts exist at all

Without them reconciliation is guesswork: two customers paying 25 USDC to the same merchant in the same block are indistinguishable to anything watching plain transfers.

The wallet path puts a paymentId on-chain. The address path gives each payment its own address. Both make reconciliation exact, and both capture the fee atomically.

Contracts

SpecieRouter — one payable call, splits and forwards both legs, emits PaymentReceived. Balance always zero. Fee capped by an immutable 1%.

SpecieFactory — derives deposit addresses and settles them. settle() is permissionless because every destination is bound into the derivation.

PaymentForwarder — deployed at the deposit address, sweeps to merchant and fee collector. It deploys real runtime code rather than returning empty: creating a contract sets the account nonce to 1, so CREATE2 collides there forever after. Without a persistent sweep(), any payment arriving after the first settlement would be stranded.

The worker

One process, several jobs, each isolated so one failing does not stop the others:

  • Relayer — settles deposits that have funds. Balance checks are batched through Multicall3;

individually they exhausted the provider's request budget and starved the watcher.

  • Watcher — indexes PaymentReceived and PaymentSettled from a persisted block cursor, so a

restart resumes rather than re-scanning.

  • Expiry and webhook dispatch — close stale payments, drain the delivery queue with backoff.

The chain is the source of truth, not the relayer. The relayer submits settlement transactions and records nothing. If it died between sending and writing, the funds would already be with the merchant while our records said pending, and the swept deposit would never be retried. Reading the event back makes that case self-healing.

Arc specifics worth knowing

Chain ID5042002 (testnet)
Gas tokenUSDC — native, not an ERC-20
FinalitySub-second

USDC has two precisions for one balance. Native (msg.value, eth_getBalance) is 18 decimals; the ERC-20 interface at 0x3600…0000 is 6. The Arc docs call mixing these up the most common porting bug. This codebase speaks 6-decimal minor units everywhere and converts in exactly one module.

No paymaster exists. A customer connecting a wallet needs USDC for gas on top of the payment. The address path sidesteps this — the sender pays their own network's fee, and we pay Arc gas to settle.

PREVRANDAO returns 0. No on-chain randomness, which is why payment ids are generated off-chain with a CSPRNG.

Native transfers revert to the zero address, precompiles and blocklisted addresses. A blocked merchant makes payments fail atomically — correct, but it must be caught at onboarding.

Recurring payments

Not built. The mechanism exists: the ERC-20 interface at 0x3600…0000 supports approve and transferFrom, so a customer could approve once and a merchant pull on a schedule, without us holding funds or custody.

It needs a subscriptions schema, a billing scheduler, an approval flow and cancellation — a milestone of its own rather than an extension of this one.