Guide
How to take payments when you don't have a company
I tried three payment providers. All three turned me down. What follows is the stack that worked anyway — and an honest account of where it stops working.
Why the obvious options fail
Stripe
Not available to individuals in mainland China, and generally requires an overseas entity — a US LLC or a Hong Kong company — plus a matching bank account. This is not a strict review; there is no application path to fail.
Paddle
Paddle is a merchant of record, so it carries the VAT and sales-tax liability. That is exactly why it runs KYB: tax responsibility sits with them, so they have to know who you are. Business registration, proof of trading, a compliant site. Individuals stall here routinely.
Creem
The application was declined on compliance, with no actionable detail. That is the pattern with these reviews — you are not told what to change.
Three different walls, one result: I could not get paid.
The question I was asking was wrong
I kept asking which gateway would accept me. The question I should have been asking is simpler: how do I confirm a payment arrived, and hand over the thing?
A gateway is just automation around those two steps — confirm, then deliver. At low volume, both steps can be manual. Once I saw it that way, the problem stopped being “find an institution that will approve me” and became “build the loop myself”.
What the build actually looks like
An orders table nobody can write to but you
id, email, github_username, status, created_at. Grant the service role write access and give the authenticated role nothing at all — no insert, no update. The buyer submits through a server route, never straight to the database.
A first-party checkout
Collect the email and the GitHub username, validate them, and write a row with status = pending. Then show your Payoneer payment link on the confirmation screen.
A delivery command, not a webhook
This is the part that replaces the gateway. One command lists the pending orders; one more fulfils a given order — invite to the private repo, send the confirmation email, mark it delivered. Make it idempotent, because you will run it twice by accident.
A secret on the delivery endpoint
openssl rand -hex 32, compared in constant time. Guess this and anyone can take your product for free.
Where this stops working
Small volume only. Tens of orders a day and manual fulfilment will bury you, and the delay is a bad experience for a buyer who has already paid. Once volume is real, you go and get the entity and move to a proper gateway.
Payment links also carry their own limits and regional restrictions. Check that yours can receive from the market you are selling into.
This is a starting stack, not a destination. Its whole value is letting you take the first payment without any of the credentials you have been told you need first.
Every one of these cost me real time
Supabase no longer auto-grants access to new tables
You create a table in public, write impeccable RLS policies, and the dashboard looks right — then the first write fails with permission denied for table. The policies were never evaluated, because the grant was missing. Every migration has to end with explicit grants. The error points at the policy, not the grant, so you will look in the wrong place for a long time.
OAuth works locally and breaks the moment you deploy
Perfect on localhost:3000, then it bounces straight back to the login page in production. Not your code — the Supabase project still has the old redirect URL allow-listed. Add the production domain under Authentication → URL Configuration, including the /auth/callback path.
A payment link has no webhook
This is worth repeating because it is where most people stall. A payment link is a person-to-person transfer tool; notifying you is not part of its design. Stop waiting for a callback that will never arrive.
Some registrars reject the CNAME Vercel gives you
NameSilo among them, for an apex domain. Point an A record at Vercel's edge IP instead. The tradeoff is that the IP is static, so you update it by hand if Vercel ever changes it.
The Vercel CLI crashes on a non-ASCII computer name
Every network call fails with "Cannot convert argument to a ByteString", because the CLI builds its OAuth user-agent from os.hostname() and Node rejects header values above 0xFF. Renaming the machine to plain ASCII fixes it permanently. Patching the CLI is undone by the next global install.
A paywall your users can delete
The one I am least proud of. I counted AI usage by counting rows in a table users had delete access to — they needed to clear their own chat history. Clearing history reset the quota. The paywall was one browser-console call from gone. Counters have to live where the user has no write access, and increment atomically.
I packaged all of it
The checkout, the delivery command, the RLS policies, the AI chat demo, and every pitfall above with its fix — as a deployable Next.js 15 codebase. It will not make you rich overnight. It compresses two or three weeks of this into an afternoon.