Say you're building a small AI tool. Users can try it for free, but once they run out of credits, they need to top up.
You've already got Stripe or PayPal in mind. Pretty standard.
Then someone asks if they can pay with USDT.
Adding another checkout button doesn't sound particularly difficult. But there's one part I'd be much more careful with: what happens between the customer sending money and your app increasing their balance.
Because nobody wants to explain why a $10 payment somehow turned into $20 worth of API credits.
A simple example: $10 for 1,000 credits
Let's keep the product boring.
You have three credit packages, and a customer chooses the $10 option. Your application needs to record the purchase, collect payment, and add exactly 1,000 credits once the payment is confirmed.
The general flow looks like this:
User buys a credit package
|
v
Create an internal order
|
v
Generate hosted checkout
|
v
Customer pays with USDT
|
v
Payment is confirmed
|
v
Verify the payment event
|
v
Add credits exactly once
The payment provider handles its part. Your application still owns the order and the customer's credit balance.
That's an important separation.
Don't start with the wallet address
One option is to display a wallet address and ask users to send USDT.
It works for receiving funds, but it doesn't automatically solve order tracking.
Imagine two customers buy the same $10 package within a minute. How does your app match each incoming transfer to the right account? What if someone pays using a different network, or sends less than requested?
For a tiny project, manual checks might be manageable. But if you're selling API credits that should appear automatically, I'd use a hosted payment checkout with a unique order reference instead.
A hosted checkout doesn't eliminate all edge cases, but it gives you a cleaner starting point.
Step 1: Create your own order first
Before calling any payment provider, create an order in your database.
Something like:
order_id: order_1042
user_id: user_87
package: starter
price_usd: 10.00
credits: 1000
status: PENDING
These are your application's fields, not the schema of a particular payment API.
I'd keep the product price and credit allocation on the server. Never let the browser tell your backend, "I paid for 10,000 credits, please add them."
The payment provider should receive the order reference and the required amount or quote. Your database remains responsible for deciding which package the customer purchased.
One more detail: if your price is in USD but the customer pays with USDT, let your payment provider's supported quoting rules determine the payable amount. Don't silently assume every stablecoin unit always equals exactly one dollar, regardless of fees or conversion.
Step 2: Redirect to checkout, not directly to fulfillment
Once the order exists, your backend can request a payment from the provider and redirect the customer to a hosted checkout page.
For a crypto checkout, the customer might select a supported asset and network, then scan a QR code or copy a payment address.
After payment, the checkout page may eventually show success.
But your frontend should not immediately add credits just because the customer landed on /payment/success.
That URL is a user interface, not payment proof.
Anyone can refresh a page. Some users may close their browser before returning. Others may return while the transaction is still pending confirmation.
The backend needs a verified payment state.
Step 3: Webhooks connect the payment to your app
This is where the automation happens.
A payment provider can send a webhook to your backend when the payment status changes.
The application should authenticate the notification using the provider's documented verification mechanism. If required, it can also retrieve the current payment state directly from the provider's API.
Before granting credits, check that:
- The payment belongs to a real pending order.
- The merchant account and provider payment reference match.
- The confirmed amount and asset satisfy the order.
- The blockchain network matches the supported payment route.
- The payment has reached the provider's required confirmation state.
- The order hasn't already been fulfilled.
The exact webhook payload, signature method, and payment statuses vary by provider, so these checks need to follow the actual API documentation.
A message saying "paid": true is not enough by itself.
Step 4: Prevent duplicate credits
Here's the bug I'd worry about most.
A payment succeeds, your backend receives a webhook, and you add 1,000 credits.
Then the payment provider retries the same notification.
If your handler simply adds another 1,000 credits, you've doubled the customer's balance without receiving another payment.
Webhook retries are normal. Your code has to be safe when they happen.
I'd keep a separate credit-grant record with a unique constraint on order_id:
CREATE TABLE credit_grants (
order_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
credits INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Now, when your backend processes a verified payment, the important part is to make the credit grant and balance update atomic.
Conceptually:
BEGIN TRANSACTION
Lock the order row
Check whether the order is eligible for fulfillment
Insert credit_grants(order_id, user_id, credits)
If this order already has a credit grant:
Do not add credits again
Otherwise:
Increase the user's credit balance
Mark the order as fulfilled
COMMIT
This is application-level pseudocode, not a copy-paste implementation.
In PostgreSQL, you can enforce the one-grant-per-order rule with the primary key, use INSERT ... ON CONFLICT DO NOTHING RETURNING, and update the balance only if a new grant was actually inserted.
The grant and balance update must happen in the same database transaction.
That gives you something much more reliable than checking a boolean in application memory.
I'd also treat webhook event deduplication separately from fulfillment deduplication. A single payment can generate different events over its lifetime, but your product should still be delivered only once for that order.
Step 5: Plan for the awkward payments
Not every payment follows the happy path.
A customer might send too little USDT, use an unsupported blockchain network, pay after an invoice expires, or close their browser halfway through checkout.
And sometimes a payment succeeds but your webhook endpoint is temporarily unavailable.
I'd rather have a few explicit states:
PENDING
PAID
FULFILLED
EXPIRED
REQUIRES_REVIEW
Not every provider uses these names. They're simply useful states for your own order system.
An exception should go into a review or recovery workflow instead of triggering automatic credits.
For missed webhooks, a scheduled reconciliation job can check unresolved orders against the provider's API and safely retry fulfillment.
The same idempotency rule should protect both webhook processing and reconciliation.
Where a payment gateway helps
If I were building this for a side project, I wouldn't start by maintaining my own blockchain nodes or building a transaction monitoring service.
I'd rather use an existing checkout integration and spend my time making sure the product-side accounting works correctly.
For transparency, I'm involved with JangoPay, which provides stablecoin payment links and API/webhook integrations for online businesses.
But the design above isn't specific to JangoPay. The same principles apply to other providers, including traditional payment processors.
Before picking one, I'd check its documentation, supported networks, confirmation rules, handling of partial payments, and webhook security.
A payment gateway is useful because it reduces how much infrastructure you have to maintain. It doesn't replace the need to build your own order logic carefully.
One last thing: subscriptions aren't the same as credit top-ups
If you're used to Stripe subscriptions, it's tempting to assume all payment methods can charge users every month.
A one-time USDT transfer doesn't automatically give your app permission to take another payment later.
For a small SaaS, prepaid credits or manual renewals are often easier to understand and implement. Automatic recurring billing needs separate support and authorization from whichever payment system you're using.
That's another reason I like the credit-top-up example. It's a manageable way to introduce an additional payment method without redesigning the entire billing system.
Final thought
Accepting a USDT payment is one problem. Making sure the correct customer receives the correct product exactly once is another.
I'd start with a hosted checkout, keep orders in my own database, verify payment status server-side, and make fulfillment idempotent.
It's not the exciting part of launching a SaaS, but it's definitely the part I'd rather get right before anyone starts paying.
If you've built a SaaS with prepaid credits, how are you handling payment retries and balance updates? I'd be interested in hearing what has worked for you.
Top comments (0)