Properties
The forwarder
InvoiceForwarder sits at a CREATE2 address salted on paymentId. Its init code commits the destination pool, so the address itself proves where the money can go.
It is sweep-only. There is no refund(), no refundTo, no refundDeadline, no Refunded event, and no window logic. Inbound funds move 100% into the pool as soon as the deposit confirms.
The forwarder stays callable after its first sweep, and this is deliberate. An earlier
refunded[token] latch was removed precisely because it could strand late funds, and late payments to already-settled addresses are an expected event — exchange withdrawal latency and re-pasted addresses both produce them. A one-shot forwarder turns every late payment into permanent unrecoverable loss.Flow
1
Quote and derive
create-pool-deposit returns the forwarder address, derived from the verified pool, plus a binding bundle. The client re-derives both and refuses to display the address on mismatch.2
Customer pays
Any transfer method. The address has no code at this point; it is just an address that will later have code.
3
Indexer observes
pool-indexer-poll watches the address. Statuses on pool_deposit_intents cover the non-happy paths: waiting, confirming, underpaid, overpaid, wrong_token, paid, swept, expired, expired_paid_late, held_sanctioned.4
Keeper deploys and sweeps
pool-sweep-keeper deploys the forwarder and sweeps. These are separate transactions today and should be merged into one, which saves the 21,000-gas intrinsic cost of the second.5
Capture
Value may be delivered only once the sweep confirms. Never on first-seen.
Cross-network and currency-agnostic acceptance
The rail accepts value, not a specific token on a specific chain. A customer who pays the right value in a different enabled stablecoin, or on a different enabled chain, is detected and reconciled rather than rejected. Wrong-token deposits get an explicit status and surface to the merchant rather than vanishing.Late payments
Expiry is a quote lock only. The counterfactual address never expires on-chain and accepts funds forever. A payment that confirms is never rejected because a timer elapsed — the chain is the source of truth. Two known gaps, both tracked: The second gap is policy rather than a bug: a post-expiry arrival is currently honoured at the expired quote’s amount with no time limit. The locked decision is to re-quote at the current rate and re-check the tolerance band instead.Minimums
Because the sponsored gas is a fixed cost per invoice while revenue is a percentage, small payments lose money. A per-chain floor exists in_shared/railEconomics.ts.
The corrected model computes everything live at quote time and, when an amount clears the connect floor but not the deposit floor, offers the connect rail rather than refusing the chain. Once the merchant bears the cost, the binding constraint becomes their configured tolerance rather than the platform’s break-even. See Who pays what.