DEV Community

Cover image for The Once-Sane Architect Who Hand-Rolled a Bitcoin Payment Rail Through Four Firewalls
Lillian Dube
Lillian Dube

Posted on

The Once-Sane Architect Who Hand-Rolled a Bitcoin Payment Rail Through Four Firewalls

The Problem We Were Actually Solving

PayPal banned us on a Tuesday. By Thursday our Stripe account was gone too. We had spent six months building a 47-page compliance deck for the regulators in Delaware, only to be evicted by the payment rails we had optimistically assumed would last until Series B. Our product was a 2 GB WebAssembly analysis engine priced at $49; customers were academics in Tehran, Kurdish NGOs in Sulaymaniyah, and indie hackers in Minsk. A six-figure AWS bill meant we couldnt eat the FX spread forever, and the payment gateways we had white-labeled inside our SaaS kept timing out on EUR→IRR swaps at 3× the mid-market rate.

We tried every hosted checkout from Lemon Squeezy to Gumroad. Every single one carried a hidden clause: if the cardholders country appears on OFACs SDN list, the transaction is auto-rejected and the merchant fee is non-refundable. One integration even returned Error 6042: Sanctions Screening Failure—manual review required. We filed the manual review; it took 17 days and was denied. Our chargeback rate on the remaining rails climbed to 1.8 % because the fraud vendors flagged every Iranian IP as high-risk regardless of actual intent. The final straw came when our bank sent a PDF titled Termination of Relationship – Code 999.

What We Tried First (And Why It Failed)

I spent a weekend building a Coinbase Commerce integration using their REST API over HTTPS. It worked for the first ten sales—until Coinbase added Iran to their restricted geo list without notice. The API returned a 403 with no body: Declined – restricted jurisdiction. We then switched to BitPays invoicing endpoint, which at least gave a JSON message: BitPay cannot process transactions for Iranian users under US banking laws. Our fallback was an iframe to NOWPayments, but their KYC flow required a government ID scan. Sending a scan across Telegram to a Belarusian node introduced its own risk surface; we leaked PII in three places before we killed the experiment.

The next idea was stablecoins on Polygon; we spun up a frontend that generated a QR code tied to a dynamic Polygon address. That lasted two days. The transaction fees on Polygon spiked from $0.01 to $0.37 during a DeFi farming frenzy, and two customers in Venezuela reported that the receiving wallet on their local exchange had been frozen for suspected money-laundering. We learned that every custodial wallet address is actually a re-hash of the exchanges hot wallet, so OFAC sanctions still propagate through the chain. My optimistic belief that immutable blockchains are censorship-resistant evaporated the moment an exchange blacklisted an entire Ethereum address range.

The Architecture Decision

We ripped out all hosted gateways and built our own Bitcoin Lightning node behind a Shadowsocks proxy hosted in a non-aligned jurisdiction. The node was a RaspiBlitz v1.9 with LND 0.15. The proxy rotated every three hours via a systemd timer and used obfs4 to defeat deep packet inspection. We priced the product in satoshis using a custom oracle that fetched Bitstamps order book every 30 seconds and added a 2 % spread to offset channel-liquidity costs. Customers generated a unique invoice hash using our backend API; we pre-warmed the channel inbound liquidity by 10 k sats so even the first payment confirmed in under 200 ms.

We stored the order in PostgreSQL with a single table: id, invoice_hash, customer_id, product_id, status. The status column was an enum limited to {pending, paid, fulfilled, refunded}. We chose eventual consistency because the Lightning networks HTLC guarantees finality; once the 30-block watchtower reported settled, we marked the order paid and queued the download link via MinIO presigned URLs that expired in 15 minutes. If the payment timed out, the invoice was automatically cancelled and the liquidity reservation released.

The retention policy for logs was 30 days; anything older was archived to Wasabi Glacier and shredded. We used Cloudflare Tunnel so the API never had a public IP; the tunnel ingress was locked to specific Cloudflare data centers in Singapore and São Paulo to avoid Russian or Iranian egress filtering. DNS was Cloudflare Registrar plus their Spectrum product to terminate TLS on the edge, stripping SNI before it hit our node.

What The Numbers Said After

In the first 90 days we processed 1,247 transactions with zero rejections for sanctions. The average confirmation time was 1.3 seconds, and the median fee paid by customers was $0.42. Our chargeback rate dropped to 0.04 % because Lightning payments are irreversible by design. However, the channel liquidity cost us 1.7 BTC in rebalancing (about $78 k at the time), and we had to hire a part-time LND operator in Tbilisi to watch for channel breaches.

The obfuscation layer added 18 ms to every API request; without it, our regional adversary could have trivially blocked the Shadowsocks ports. The Bitstamp oracle latency once slipped to 5.2 seconds during a regional AWS outage, and one payment failed to confirm within the 10-minute invoice expiry window. That user was a PhD student in Isfahan who had to re-pay; we manually refunded the failed transaction and issued a new invoice. The overall refund rate settled at 2.8 %, which was acceptable given the geo-restrictions we avoided.

What I Would Do Differently

If I rebuilt it today, I would run two Lightning nodes—one small for retail orders and a second larger one dedicated to institutional buyers who pay in bulk. I would also bake in support for the Lightning Address protocol so customers can auto-settle via Lightning wallets without scanning QR codes. Id drop the obfs4 layer and instead rely on Cloudflares new CAPTCHA-less browser challenges; the cat-and-mouse game with DPI is exhausting.

I would also implement a watchtower client on my node to monitor the 30-block safety window; the open-source watchtower I used initially leaked the invoice pre-image if memory spiked over 2 GB, causing a node restart. I would switch the database to CockroachDB instead of PostgreSQL so we could geo-partition the orders table and survive a regional cloud

Top comments (0)