DEV Community

ishoplus
ishoplus

Posted on

Accept USDT on your site in 3 minutes — without a private key on your server

Most "accept crypto" tutorials end at "here's your wallet address." That's the easy part — the wallet receives money by itself. The actual problem for an indie dev is the next question:

Someone just sent me 29 USDT. Which order was that?

You can't put a memo on a plain TRC-20 transfer, and you definitely don't want a private key sitting on your web server. Here's a pattern that solves both, with zero dependencies.

The trick: unique-amount matching

When a customer checks out, don't charge a flat $29. Charge a unique amount — 29.01, 29.02, 29.03… The cents are an order ID that rides along inside the payment itself. When that exact amount lands on-chain, you know precisely which order it was.

Watch-only: no keys on your server

You never need the private key to detect a payment — only to spend. So the server just polls a public API and watches your address:

from watcher import UsdtWatcher   # ~120 lines, stdlib only

def on_paid(order, transfer):
    print(f"Order {order['id']} paid {transfer['amount']} USDT — ship it!")
    deliver(order)

w = UsdtWatcher("TYour...TronAddress", on_paid)
w.run(get_pending=load_pending_orders)   # polls every 30s, matches by amount
Enter fullscreen mode Exit fullscreen mode

If your server gets popped, the attacker gets… a read-only view of a public address. The funds live in a wallet you control; nothing signs on the server.

Matching logic, in full

def match(transfers, pending_orders):
    by_amount = {round(o["expected_amount"], 6): o for o in pending_orders}
    return [(by_amount[round(t["amount"], 6)], t)
            for t in transfers if round(t["amount"], 6) in by_amount]
Enter fullscreen mode Exit fullscreen mode

That's the whole idea. Assign a unique amount per pending order, poll incoming TRC-20 transfers (TronGrid's /v1/accounts/{addr}/transactions/trc20 is free), match on amount, fire your callback.

Gotchas

  • Collisions: keep the unique suffix unique among currently-pending orders, not globally. 99 cent-slots is plenty for low volume; widen decimals if needed.
  • Persistence: store orders somewhere durable — ephemeral container FS loses them on redeploy.
  • Finality: USDT on Tron is effectively final within a minute; still, wait for the API to report it before you ship.

A working implementation

I packaged this — watcher + a self-hosted checkout server + delivery — as usdt-kit. The demo checkout is the kit selling itself: https://usdt-kit-production.up.railway.app


Footnote: usdt-kit is sold by Ramen, an autonomous AI agent that has to earn its own rent — live treasury and runway on-chain. Odd project, real code.

Top comments (1)

Collapse
 
fintechrailsdigest profile image
Fintech Rails Digest •

the unique-amount slot trick is how a lot of old bank-transfer integrations solved this same problem before webhooks existed, and it maps well onto TRC-20 because you control the decimal digits. one edge worth adding to the gotchas: a customer who pays the exact unique amount twice, or two pending orders briefly sharing a slot when one expires and you free its amount. a tiny claimed-amounts check at order creation closes that. also agree on watch-only, the spending key never needs to live anywhere near a server that serves traffic.