Blog ·
One switch, two processors
Until this week, everything BearLog sells was priced in pesos and paid through PayMongo. That was the right call. PayMongo is the domestic rail: QR Ph, GCash, Maya, the local banks, and cards issued here. For a queue system sold to a barangay clinic or a Memory posted to a fridge in Tubao, there is nothing better.
It is also a wall. A viewer in Manchester who lands on a Page and wants the Galaxy effect cannot pay for it. Their card is not what the rail is built for, and even when it goes through, the price is a peso figure their bank converts at a rate neither of us chose. The Memories TikTok account already gets most of its views from outside the Philippines. The front door was open and the till was not.
Why not just add Stripe
Because taking a card from abroad is the small problem. The large problem is that the moment you sell a digital good to someone in the EU, the UK, Australia or one of forty-odd US states, that jurisdiction considers you a seller in it, and would like its VAT, GST or sales tax collected, filed and remitted, on their form, in their timezone. For a one-person company registered with the DTI in La Union, selling an unlock for the price of a coffee, that is not a cost of doing business. It is the business.
A merchant of record takes that job. Lemon Squeezy is the seller on the receipt. They charge the card, work out which tax applies, add it, file it, and eat the chargeback if one comes. We get an order webhook and a payout. The fee is higher than a raw processor’s, and it is the cheapest accountant I will ever hire.
What crosses the border, and what does not
Only files. An Unlock, a Memory sold as the print-ready file, a creator batch, a booth band. Anything we physically print and post stays inside the Philippines, and the code refuses the order before checkout rather than after payment:
// app/api/orders/route.ts
if (country !== null && country !== "PH" && quantity > 0) {
return json({ error: "Printed items post within the Philippines only." }, 403);
}
The address form is the second fence. It only knows how to describe a barangay, a province and a Philippine postcode. There is no field in which to type “Manchester”, so the question of shipping there never gets asked.
Where the buyer is
Vercel puts the request’s country in a header, and that header decides the processor:
// lib/geo.ts
export const country = (h: Headers) => h.get("x-vercel-ip-country");
// lib/payments.ts
const abroad = country !== null && country !== "PH";
Unknown counts as home. If the header is missing, which happens on localhost and in a few odd proxies, the buyer sees pesos and PayMongo, the path that has been live since June. The new path is the exception, not the default, so a broken header degrades to the thing that already works.
Pricing in a currency we do not think in
Lemon Squeezy’s store runs in dollars, so each thing sold abroad has its own dollar price rather than a peso price passed through a rate. Those are prices, chosen, not conversions, and a new item cannot silently sell for whatever the peso did that morning.
One shape coming back
Two processors, two webhook formats, two signature schemes, and only one question that matters: which row is now paid. So both webhooks end in the same place.
// lib/settle.ts
type Settled = { ref: string; kind: SKU; id: string; amountCents: number };
export async function settle(s: Settled) {
switch (s.kind) {
case "unlock": return grantUnlock(s.id, s.ref);
case "memory": return markMemoryPaid(s.id, s.ref);
case "batch": return markBatchPaid(s.id, s.ref);
case "booth": return markBoothPaid(s.id, s.ref);
}
}
PayMongo’s checkout_session.payment.paid and Lemon Squeezy’s
order_created each get a thin parser that checks the signature and
produces a Settled. Everything after that line is shared, tested once, and
does not know or care which country paid.
The Lemon parser does one thing the PayMongo one never had to. The checkout URL carries the price we expect as custom data, and the webhook compares the order’s subtotal against it. A hosted checkout page is a page, and a page can be fiddled with. If the subtotal does not match the cents we sent, the order is logged and not settled, and a human looks at it.
Hosted checkout everywhere
The change that came along with this is the one Philippine buyers will notice. Paying used to mean a QR Ph code drawn inside our own dialog. Now every purchase, at home or abroad, redirects to the processor’s hosted page and comes back. On PayMongo that page offers whatever the account has switched on: QR Ph, GCash, Maya, cards. Adding a method is a change to one environment variable, with no deploy, which matters when a wallet goes down on a Saturday and the fix is to turn on the other one.
The cost of a redirect is that the buyer leaves. Every purchase screen now
remembers what it was waiting for, in sessionStorage, and on return picks
up the same poll it was running before. Close the tab and reopen the
Memory, and it is still asking the same question, is this paid yet, until
the answer is yes.
The same week, in Queue and Heimdal
The hosted page landed in Queue and Heimdal the same weekend, for the plan a business pays for and the watch page a guardian unlocks. Those two are domestic products, sold in pesos to Philippine organisations, and stay on PayMongo alone. What they gained is the method list. A school office that pays from a corporate card was, until Saturday, being handed a QR code meant for a phone wallet.
What it costs
Two processors, one switch. A Memory bought in Manchester arrives as the same print-ready file it does in Tubao, the buyer takes it to a shop near them, and the piece on their fridge plays the same video when a phone comes near it. The printing was never the product. The file was, and a file has no customs form.