How to Give Every User Their Own USDT Deposit Address
Why one shared address breaks down, how a deposit address per user works, and how to credit deposits automatically with one API call and a webhook.
An exchange, a game, a marketplace or any app with balances has the same problem on day one: a payment arrives, and you have to know which user sent it. This guide shows the approach nearly every large platform uses, a deposit address per user, and how to add it to your product without building wallet infrastructure.
Why a single shared address does not work
- A blockchain transfer carries no name. With one address for everybody, you can only guess the sender from the amount or ask users to paste a transaction hash.
- Memos and reference codes get forgotten or mistyped, and every mistake becomes a support ticket about missing money.
- Two users who send the same amount at the same time cannot be told apart at all.
With an address per user the question disappears. Only one user was ever shown that address, so whatever arrives there is theirs.
What it takes to build yourself
Generating addresses is the easy part. You also have to store keys safely, watch every address on every network, wait for confirmations, collect the funds from thousands of addresses into one wallet, and keep gas on each of them to do so. Deposit wallets are that whole pipeline behind one call.
Step 1: create an address when a user signs up
Create a free account, open the Deposit Wallets project in the dashboard and copy its API key. Then create a wallet for the user. external_id is your own id of the user. It is how you will recognise their deposits.
curl https://api.lirchain.com/merchant/deposit-wallet/create \
-H "x-api-key: YOUR_API_KEY" \
-H "content-type: application/json" \
-d '{
"network": "polygon",
"asset": "USDC",
"external_id": "customer-7731"
}'const API_KEY = "YOUR_API_KEY";
async function createDepositWallet(params) {
const response = await fetch("https://api.lirchain.com/merchant/deposit-wallet/create", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`LirChain API ${response.status}: ${await response.text()}`);
}
return response.json();
}
const result = await createDepositWallet({
network: "polygon",
asset: "USDC",
external_id: "customer-7731",
});const API_KEY = "YOUR_API_KEY";
type CreateDepositWalletParams = {
network: string;
asset: string;
external_id?: string;
};
type CreateDepositWalletResult = {
id: string;
project_id: string;
merchant_id: string;
network: string;
address: string;
asset: string;
external_id: string | null;
};
async function createDepositWallet(params: CreateDepositWalletParams): Promise<CreateDepositWalletResult> {
const response = await fetch("https://api.lirchain.com/merchant/deposit-wallet/create", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error(`LirChain API ${response.status}: ${await response.text()}`);
}
return response.json();
}
const result = await createDepositWallet({
network: "polygon",
asset: "USDC",
external_id: "customer-7731",
});import requests
API_KEY = "YOUR_API_KEY"
def create_deposit_wallet(**params):
response = requests.post(
"https://api.lirchain.com/merchant/deposit-wallet/create",
headers={"x-api-key": API_KEY},
json=params,
timeout=30,
)
response.raise_for_status()
return response.json()
result = create_deposit_wallet(
network="polygon",
asset="USDC",
external_id="customer-7731",
)<?php
const API_KEY = "YOUR_API_KEY";
function create_deposit_wallet(array $params): array
{
$curl = curl_init("https://api.lirchain.com/merchant/deposit-wallet/create");
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($params),
CURLOPT_HTTPHEADER => ["x-api-key: " . API_KEY, "content-type: application/json"],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($body === false || $status >= 400) {
throw new Exception("LirChain API $status: $body");
}
return json_decode($body, true);
}
$result = create_deposit_wallet([
"network" => "polygon",
"asset" => "USDC",
"external_id" => "customer-7731",
]);package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const apiKey = "YOUR_API_KEY"
func CreateDepositWallet(params map[string]any) (map[string]any, error) {
body, err := json.Marshal(params)
if err != nil {
return nil, err
}
request, err := http.NewRequest("POST", "https://api.lirchain.com/merchant/deposit-wallet/create", bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("x-api-key", apiKey)
request.Header.Set("content-type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
if response.StatusCode >= 400 {
return nil, fmt.Errorf("LirChain API %d: %s", response.StatusCode, data)
}
var result map[string]any
return result, json.Unmarshal(data, &result)
}
func main() {
result, err := CreateDepositWallet(map[string]any{
"network": "polygon",
"asset": "USDC",
"external_id": "customer-7731",
})
fmt.Println(result, err)
}The answer contains the address. A wallet accepts one asset on one network: USDT or USDC on Ethereum, BNB Chain or Polygon, or the network's own coin (ETH, BNB, POL). Create one per network you want to offer, for example USDT on BNB Chain and USDT on Polygon.
Step 2: show the address
Show the address and the network in your own deposit screen, or send the user to the ready-made page at lirchain.com/deposit/<wallet id>, which has the address, a QR code and the latest deposits. The address is permanent, so the user can save it and top up again later.
Step 3: credit the user from the webhook
Open the project in the dashboard, paste the HTTPS address of your endpoint into its webhook field and save. Every confirmed deposit is then posted to it:
{
"type": "deposit-wallet",
"deposit": {
"id": "41b0d4fe-8b49-4623-bd8f-6f04586b06c9",
"wallet_id": "3f4911a7-2c26-4f9c-a97c-80b25b6feb8c",
"external_id": "customer-7731",
"project_id": "3d6b1130-da92-4ddf-bad2-93f898993c39",
"network": "ethereum",
"asset": "USDT",
"contract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"amount": "250.5",
"pay_amount": "250.5",
"fee": "1.2525",
"receive_amount": "249.2475",
"created_ms": 1791503990112
}
}external_id is the user. pay_amount is what the user sent, in dollars, fee is the fee, and receive_amount is what was added to your balance. Credit the user pay_amount if you absorb the fee, or receive_amount if you pass it on.
import express from "express";
const app = express();
app.use(express.json());
// Use a table in your database instead of memory in production
const handled = new Set();
app.post("/webhooks/lirchain", async (req, res) => {
const id = req.get("x-webhook-id");
if (handled.has(id)) {
return res.sendStatus(200);
}
handled.add(id);
if (req.body.type === "deposit-wallet") {
const { external_id, pay_amount, asset, network } = req.body.deposit;
console.log(`credit $${pay_amount} to ${external_id} (${asset} on ${network})`);
}
res.sendStatus(200);
});
app.listen(3000);Answer with a 2xx status within 10 seconds. If your server does not, the same webhook is sent again every minute, up to 20 times, each time with the same x-webhook-id header, so keep the ids you handled and skip repeats.
Webhooks are not signed. Before you credit a user, read the deposit back from the API with your key (the deposits list, filtered by the wallet) and credit what the API says.
What happens with ETH, BNB and POL
A deposit in a network's own coin is exchanged into dollars when it arrives, usually within a few minutes, and what the exchange paid is what the deposit is worth. Your balance is always in dollars, so you never hold a coin whose price moves.
Costs and limits
- Creating addresses is free. A deposit costs the percentage of your plan, from 1% on Free down to 0.25% on Enterprise, at least $0.10.
- The Free plan includes 100 deposit wallets.
- A deposit worth less than $1 is not counted, so tell your users the minimum.
- A deposit is counted once it is 12 blocks deep.
Questions and answers
- Can a user reuse the same deposit address?
- Yes. A deposit wallet is permanent and every transfer to it is a new deposit, so it works for repeated top-ups.
- How do I get the money out?
- Everything is added to one balance in dollars. You withdraw it from the dashboard as USDT or USDC on Ethereum, BNB Chain or Polygon, or send it to your users with the Payouts API.
- Do I have to keep gas on every deposit address?
- No. Collecting the funds from the addresses is done for you, and you never hold or top up gas on them.
Try it on the Free plan
Every product is included, and no card is needed to sign up.
Create a free account