DEV Community

Cover image for The Font Vendor Who Wouldnt Take No for an Answer
mary moloyi
mary moloyi

Posted on

The Font Vendor Who Wouldnt Take No for an Answer

The Problem We Were Actually Solving

In 2023 I ran a small type foundry that sold fonts under the table because every mainstream payments provider refused our IP code. Stripe wanted a U.S. bank, PayPal wanted a Delaware LLC, Gumroad choked on our IP, and Payhip locked us out because our country of incorporation wasnt on their whitelist. Payments were still coming in by crypto or bank wires, but the friction was killing us: ten minutes of manual bookkeeping for every license and chargebacks that took weeks to resolve. Our biggest buyer was a game studio in Japan, and their finance team refused to open a letter of credit for what they called a boutique font shop. Something had to change.

What We Tried First (And Why It Failed)

We bolted together a WooCommerce shop on a VPS in Amsterdam that took Monero and bank transfers. WooCommerces REST API was fine, but the analytics dashboard looked like a Rube Goldberg machine: every new Monero address had to be tagged by hand in a Google Sheet, and at 2 a.m. one Sunday our Lets Encrypt cert expired because certbot couldnt reach the domain over IPv6. The storefront died, the game studios accountant called screaming, and I spent two hours on a VPN from a café near Hauptbahnhof debugging DNS glue records until the hosting provider finally rolled back the snapshot at 4:17 a.m.

We then flipped the switch to Open edX with a plugin called edX-Commerce that accepted Bitcoin via BitPay. The plugin was twelve years old, the README warned in ALL CAPS that it only worked on Python 2.7, and BitPays fee was 1% plus network fees that swung by 300 bps some weeks. After Black Friday our Redis cache wedged because the plugin spawned a new thread for every unconfirmed transaction and we ran out of file descriptors. The edX-Commerce maintainer had stopped merging PRs in 2021, so we forked the whole thing, renamed it to Py3-compatible code, and woke up to 3,200 unprocessed orders stuck in a Kafka topic with no consumer running. The game studios invoice bounced for the third time.

The Architecture Decision

We finally settled on a headless shop built on Saleor v3.14 running on Fly.io. Saleors GraphQL API gave us a single source of truth for inventory, invoices, and receipts, and Flys Postgres HA with automatic failover meant we didnt have to babysit pgbouncer at 3 a.m. For payments we routed everything through a small Rust service called tender that wrapped a tiny embedded wallet for Monero, a PayID endpoint for bank transfers, and a Lightning node for Bitcoin. Tender published every payment event to a NATS topic, and a separate worker consumed the topic to generate licenses in a Redis stream keyed by the buyers email. The worker also sent a webhook to Discord so the design team could celebrate without paging me.

What The Numbers Said After

After six weeks the system handled 1,800 orders with zero chargebacks and an average latency of 420 ms for the checkout GraphQL call. The Monero wallet never re-orged more than two blocks, the Lightning node settled in under two minutes on 92% of attempts, and our bookkeeping went from ten minutes per order to three seconds because Saleors invoice PDFs matched the NATS event stream exactly. Flys Postgres had one failover during a Frankfurt outage, and the sale kept going because the read replica picked up the next NATS message within 1.3 seconds. The game studios CFO finally signed off after we delivered an ISO-compliant invoice in under twelve hours instead of the usual three weeks.

What I Would Do Differently

I should have started with Saleor earlier instead of treating payments as an afterthought. Saleor forced us to model the domain correctly—licenses, invoices, and refunds are all first-class entities—so the payment plumbing became a plugin, not a tangle of scripts. I also wouldnt have written Tender in Rust; the borrow checker saved us from a race condition that would have corrupted the Redis stream, but the compile times added a week to the release cycle. Next time Ill write the worker in Go and keep the Rust for the wallet itself. Finally, I would have budgeted for a Fly.io dedicated Postgres cluster from day one; the hobby tiers shared CPU became a bottleneck when we hit 500 concurrent checkout sessions during a Steam sale.

Top comments (0)