Phase 3: Multi-EVM Rollout and Keeper Redundancy
April 20, 2026 — Phase 3 takes the contract surface from Phase 1 and Phase 2 and puts it on every EVM chain we support. It also adds a second keeper, so a NestJS outage cannot skip a subscription charge.
TL;DR
- Deploy
PaymentLinkFactory,PaymentLinkReceiverimplementation,FeeSplitter, andSubscriptionHubto Ethereum mainnet, Arbitrum, Optimism, and Polygon. - Same CREATE2 salt scheme everywhere — the factory address is the same on every chain (clean for docs, clean for integrations, clean for
computeAddressfrom the client). - Register Chainlink Automation (preferred) and/or Gelato Web3 Functions as a second keeper calling the same
charge(ids[]). - Publish SLOs: 99.5% of charges within 10 minutes of due time, measured.
What this phase changes
Multiple keepers calling the same public charge() is the liveness story. None of them has custody; any of them can save a due charge.
Deployment plan
Deploy order (one chain per week, canary per chain):
- Ethereum mainnet (chainId
1) - Arbitrum One (
42161) - Optimism (
10) - Polygon PoS (
137)
Addresses on each chain are published in /docs/non-custodial/contract-reference. The deployer key is the same 3-of-5 Safe multisig defined in Phase 0, redeployed per chain from the same ceremony.
All four chains share the same useOnChainReceiver feature flag scoped by chainId inside api/src/payments/wallet.service.ts. Merchants opt in chain-by-chain; defaults flip to on once the canary week is clean.
Gas strategy for charge()
The keeper's charge() transactions need gas. We reuse the existing paymaster plumbing in api/src/withdrawals/services/gas-strategy:
- Alchemy paymaster enabled (Base, Arbitrum, Optimism, Polygon): keeper calls
SubscriptionHub.charge()as a UserOperation with policy sponsorship. Zero gas cost for the keeper EOA. - Ethereum mainnet (no paymaster): keeper self-funds from its hot-gas balance. Funded daily by the platform wallet; monitored with a Datadog alert for low balance.
- Merchant-subsidized gas (optional): merchants can set a
gas_refund_addressand the keeper pulls reimbursement from there, matching the existing support/commission pattern. Out of scope for initial rollout.
Second keeper: Chainlink and Gelato
The second keeper is a redundancy play, not a replacement. It runs alongside the NestJS scheduler.
Chainlink Automation (preferred on EVM)
- Register an Upkeep per
SubscriptionHub. checkUpkeep()returnsperformData = abi.encode(ids[])for allidwithblock.timestamp >= nextChargeAt(read from a view function).performUpkeep(performData)callscharge(ids)in a batch.- Funded with LINK from the platform treasury; balance monitored.
Gelato Web3 Functions (alternative)
- A JS Web3 Function polls the hub, finds due IDs, and submits a
charge(ids[])transaction. - Pricing is post-paid from Gelato's
1Balance.
Either one is sufficient on its own; we will pick based on operational experience during Phase 3. Running both is fine — charge() is idempotent within a period thanks to lastChargedAt.
SLOs and monitoring
Published SLO targets:
| Metric | Target |
|---|---|
charge() within 10 minutes of nextChargeAt | 99.5% |
charge() within 1 hour of nextChargeAt | 99.9% |
| Keeper EOA hot-gas balance above alert threshold | 99.99% |
| Events indexed within 2 minutes of block finality | 99.5% |
Monitoring hooks into the existing Sentry setup documented in the repo's Sentry workflow skill. Alerts:
charge()reverts per hour > threshold → PagerDuty- Keeper EOA native balance below N gwei × gasLimit × 100 → PagerDuty
- Chainlink/Gelato upkeep unhealthy → warning
Paidevent indexer lag > 5 min → warning
Deprecations
Same as Phase 1 and 2, but on more chains. Nothing is deleted yet — deletion waits for Phase 7.
Migration plan
- New payment links: use contract receivers on any chain with the flag on.
- New subscriptions: use
SubscriptionHubon any chain with the flag on. - Existing customers: opt-in migration at renewal, same as Phase 2.
- Cross-chain settlement (Relay): unchanged surface; the keeper still calls Relay executors in api/src/withdrawals/services/bridge, just from an EOA that has no custody.
Risks and mitigations
- Multi-chain address drift. Mitigation: deterministic CREATE2 deployer (e.g.
0x4e59b44847b379578588920cA78FbF26c0B4956C) with a locked init-code hash; factories share an address across all four chains. - Per-chain paymaster policy limits. Mitigation: per-chain monitoring and policy quota alerts at 70% usage.
- Chainlink upkeep registration churn. Mitigation: one upkeep per chain; the list of IDs is read on-chain, so merchant onboarding does not require Chainlink config changes.
- Double-charge across keepers. Mitigation:
lastChargedAtmonotonic plusChargeSkippedfor duplicates; both keepers are idempotent by design. - Mainnet gas spikes. Mitigation: per-chain batch size tuning; a gas-price ceiling above which
charge()is deferred one block.
Status checklist
- Deploys completed on Ethereum, Arbitrum, Optimism, Polygon
- Factory + hub addresses published in contract reference
- Paymaster policies validated per chain
- Chainlink Automation upkeep live on at least 2 chains
- SLO dashboard in Datadog
- Runbook for keeper outage (manual
charge()from merchant CLI) - Canary merchants rolled per chain
What is next
Solana is the other big chain we must make non-custodial: Phase 4.
Reference docs: Phase 3 status · Keeper strategy · Contract reference
