A production payments platform for e-commerce merchants. Bank-to-bank transfers in place of card processing, with customer rewards funded by the interchange savings.
Card interchange is a tax on every e-commerce transaction. For a merchant doing $10M/year, Visa and Mastercard fees alone consume $200K–$300K in margin — money that goes to networks and issuers, not the business.
The alternative — ACH and open-banking transfers — has existed for years. But adoption is blocked by three real engineering problems, not marketing ones: bank-account-based payments feel slow and unfamiliar at checkout; settlement is asynchronous and reversible in ways most teams underestimate; and integration cost is high — merchants will not rebuild their checkout for a new processor.
Our engagement: build the full platform end-to-end. Backend, infrastructure, merchant-facing SDK, and the embedded checkout experience.
“The interesting work isn't in shipping bank-to-bank payments. It's in shipping them in a way that drops into a merchant's existing checkout the way Stripe does — or it loses before it starts.”
A drop-in payment method for e-commerce merchants. Customers click Pay with Magic on the merchant's store, authenticate their bank in a Magic-hosted flow, and funds move bank-to-bank. Merchants pay a fraction of card processing fees and can route the savings into customer rewards — tokens, cashback, store credit — to incentivize the switch.
The platform is four moving parts: a thin TypeScript SDK on the merchant page, a hosted checkout on a Magic-controlled domain, a Django backend organized around an event-sourced ledger, and external rails (Plaid for bank linking, Dwolla for fund movement) talked to through a webhook + event orchestrator that treats retries as expected, not exceptional.
One npm install. One component. The SDK opens a Magic-hosted page in a controlled child window (popup with iframe fallback for blocked contexts) and returns a signed result via postMessage. Nothing else. Every additional method on a public SDK is a future support burden.
This isolation matters for three reasons: PCI and compliance scope (merchant never touches bank credentials), upgrade path (we can change the entire checkout flow on our side without merchants redeploying), and trust model (customer enters credentials on a domain that visibly belongs to a payment provider, not a random storefront).
Every transfer moves through initiated → submitted → pending → settled → finalized, with explicit paths to failed, returned, and disputed. State transitions are append-only events on a ledger table, never destructive updates on a transactions table.
The ledger is the source of truth, not the bank's API response. Dwolla's transfer endpoint is the trigger; reconciliation against Dwolla webhooks, periodic transfer status pulls, and bank return files determines the actual state. Current state is computed, not stored as ground truth.
Every transfer carries a deterministic idempotency key derived from (merchant_id, customer_id, order_id, amount, nonce). Retries — from the SDK, from webhook redelivery, from manual replays — collapse to the same transaction at insert time. The key is enforced at the engine layer, not bolted onto HTTP handlers.
The payment ledger and the rewards ledger are independent systems. Every reward grant references the originating transaction by ID but settles on its own state machine, with its own idempotency model. When a transaction reverses, the reward is automatically clawed back through the same event mechanism — not through application logic that someone has to remember to write.
The bookkeeping cost of this separation is higher. The cost of not doing it shows up six months in, when a merchant disputes a settlement total and there's no clean way to prove the numbers.
A few views from the production pipeline. Some details have been redacted for client confidentiality.


Every architectural decision had 2-3 alternatives we seriously considered. Here are the approaches we rejected and why:
Naive version: subtract the reward at transfer time. Creates accounting problems within a quarter — refunds and reversals require unwinding two intents in a single ledger row, and merchant statements stop reconciling to bank statements.
Cheaper to build. But settlement timing diverges from reward timing, and disputes become impossible to debug because the same row carries both concerns. Six months in, you can't prove a merchant's settlement total.
The transfer engine, the idempotency check, and the ledger writer are deliberately the most boring code in the system. Cleverness lives at the edges — SDK, integration surface, developer tooling. Not where money moves.
Dwolla's success response means instructions accepted, not money moved. Treating it as ground truth produces a system that silently disagrees with reality every time a transfer returns 1–4 days later.
The platform launched into production handling live merchant transactions across the full flow: bank linking, authorized transfers, reconciliation, rewards distribution, and refunds. The SDK shipped on npm and integrated with merchant storefronts in under an afternoon per integration.
The technical decisions documented above held through scale. The event-sourced reconciliation model, in particular, paid back the upfront cost the first time a settlement question came in from a merchant — and every time after. The question "what actually happened to this $4,127 transfer last Thursday?" became a query, not an investigation.
The separation of payments and rewards ledgers showed its value the first time a merchant disputed a settlement total. With independent state machines and a shared event mechanism for reversals, the system produced a complete, defensible timeline for both ledgers without any reconciliation logic written by hand.
“The boring code in the critical path is the reason we can answer settlement questions in seconds instead of days. Cleverness at the edges, discipline at the core.”
Looking back: the highest-leverage decision was treating the ledger as an append-only event log from day one, not retrofitting it later. Every payments platform eventually arrives at event sourcing for the money path. The teams that start with row-mutation accounting spend a year unwinding it before they can ship reconciliation features.
The second-highest was keeping the SDK intentionally thin. Every additional method on a public SDK becomes a future support burden — and merchant integrations are where the platform's reputation lives or dies. A small surface area is easier to support, easier to upgrade, and easier to reason about during compliance reviews.
If we started today, the only change we'd make is investing earlier in dead-letter queue tooling for unprocessable webhooks. We built it in week 6; doing it in week 1 would have saved roughly a week of incident triage on the early integrations.
First call is 30 minutes. You describe what you're trying to ship and what's in the way. We ask technical questions and figure out together whether we're the right team for it.