DEV Community

Cover image for The Checkout That Broke Everywhere Else
theresa moyo
theresa moyo

Posted on

The Checkout That Broke Everywhere Else

The Problem We Were Actually Solving

In May 2025 we launched a small imprint, BytePress Books, to sell PDF editions of niche CS monographs. Our first title, Zero-Knowledge Algorithms for Practitioners, had a two-page preview in WebAssembly, a 200-page PDF, and a supplementary Jupyter notebook. The file size was 2.8 MB, so we couldnt just email it; we needed a proper, automated download after payment. Our user base skewed toward researchers in sanctioned regions who could not open a Stripe account even if they wanted to. Week one we processed 47 sales; week two the number dropped to 12 because two gateway providers locked entire geographies overnight. We were losing credibility faster than we were losing money.

What We Tried First (And Why It Failed)

We started with Stripe Checkout embedded in a Next.js frontend and a Firebase Cloud Function that fired a signed URL to S3 on payment success. The flow worked fine from a MacBook in California, but when we spun up a test user in Tehran the Stripe popup blocked with:

This payment method is not supported in your country.

We tried Payhips global checkout; same error. Gumroads checkout failed with Address verification required even though we only needed an email. PayPal Payouts returned Receiver country code not supported for Nigeria. Each platforms documentation listed the exact countries it excluded, but none documented the cascading effect when a users billing address matched two exclusion lists simultaneously.

Our engineering mistake was assuming the problem was the download link, not the payment trigger. We spent three days instrumenting Firebase Functions logs for 400 ms latency spikes; the real latency was between the browser and Stripes /v1/tokens endpoint when the IP originated from an embargoed ASN.

The Architecture Decision

We ripped out every payment provider that required a billing address. Instead we built a wallet-first flow:

  1. User clicks Buy in our Astro site.
  2. We generate a Monero address (XMR) using the monero-wallet-rpc service running on a 2 vCPU DigitalOcean droplet in Amsterdam.
  3. The frontend polls a Go service (paywall-service) every 5 seconds for a WebSocket message confirming a transaction with 10 confirmations (≈20 minutes).
  4. On confirmation, paywall-service calls Cloudflare Workers KV to store a single-use JWT that unlocks the PDF for 24 hours. The JWT contains user email, a SHA-256 hash of the Monero transaction ID, and an expiration timestamp. The PDF itself is hosted on a Wasabi bucket fronted by Cloudflare R2 to avoid egress fees in restricted regions.
  5. We log nothing beyond email and tx_id; no IP, no country, no device fingerprint. This keeps us below the threshold where KYC providers would flag us.

Trade-offs:

  • Monero transaction confirmation time is longer than Stripes 2-second auth, but it moves value without an address.
  • Astros static build meant we had to hydrate the WebSocket client in a separate bundle, adding 32 KB to the critical path.
  • We now run our own Monero node because third-party explorers blacklist certain IP ranges; maintaining the node costs $42/month in bandwidth alone.

What The Numbers Said After

After four weeks the new flow processed 312 sales across 48 countries that Stripe had quietly dropped in June 2025. Average confirmation time was 18 minutes 43 seconds versus Stripes sub-2 seconds when it worked. We instrumented the checkout with OpenTelemetry traces; the slowest span was always the Monero daemon RPC call, not our own code. Conversion dropped from 2.8 % on Stripe to 1.9 % on Monero because users balked at the 20-minute wait, but that single trade-off was acceptable once we could actually close sales in Iran.

Support tickets fell from 14 per week to 2 because we stopped asking for addresses. Chargebacks became meaningless; Monero is irreversible by design. Revenue in USD terms grew 340 % month-over-month because we finally reached readers who mattered.

What I Would Do Differently

I would not have built the Monero node myself. Instead I would have used a custodial Monero payment processor like XMR.to or MyMoneros Business API. The nodes sync time lagged during a hardfork in April 2025 and we missed two sales while our daemon was 12 blocks behind. If we had outsourced custody we would have paid a 1 % fee but saved three engineering days of node ops.

Second, I would have front-loaded the PDF unlock logic. Our first version generated the JWT only after 10 confirmations, but users expected instant gratification. We added an optimistic unlock after 3 confirmations (≈6 minutes) with a banner: Your book will be ready in ~14 minutes unless you reload. Conversion on that single line of JavaScript increased revenue by 12 % despite the temporary double-trigger risk.

Finally, I would have instrumented the Monero transaction polling loop with a circuit-breaker pattern. Three times our Go service hammered the DigitalOcean API because a single stuck goroutine leaked file descriptors. Adding a 5-second backoff with Prometheus alerts would have prevented the pager incidents that woke me up at 3 AM.

Top comments (0)