Skip to content
Crypto Merchant

Developers

How to Add Crypto Top-Ups to Customer Accounts: Per-Customer Addresses, Webhooks and Crediting

Let customers top up an account with crypto: permanent addresses per customer, when to credit, how to avoid double credits, fees and what you must build.

By the Crypto Merchant team10 min read

To let customers top up an account with crypto, give each customer their own permanent deposit address, and credit their balance when your server receives a signed "deposit confirmed" webhook, once per deposit ID. Unlike an invoice, a top-up has no set amount and no expiry, so the customer can send any amount at any time. Crypto Merchant provides this as sticky wallets, chosen at sign-up, with a standard fee of 1% of each deposit and no monthly fee. It does not hold your customers' balances for you: that ledger lives in your own system.

Last checked: 6 November 2026.

When do top-ups fit better than invoices?

Use top-ups when the customer holds a balance with you and spends it later. Use invoices when each payment belongs to one order.

InvoicePayment linkSticky wallet (top-ups)
AmountFixed by youFixed or chosen by the customerWhatever the customer sends
AddressIts own, while the invoice is openIts own for each payment, while the invoice it creates is openPermanent, for that customer only
Expiry30 minutes by defaultThe invoice it creates expiresNone
Who is identifiedYour order IDWhoever paysYour customer ID, on every deposit
Underpaid or overpaidHandled on the invoice pageHandled on the invoice pageDoesn't apply: every amount is a deposit
Refund linkYes, once paidYes, once paidNo: see below
Good forShops, one-off ordersNo website, small sellersPrepaid credits, hosting and ad balances, marketplaces, usage-based SaaS

A business on sticky wallets can send invoices too. For the invoice route, see accepting crypto through the API.

How does a crypto top-up work?

  1. 1A customer opens the deposit page on your site.
  2. 2Your server calls POST /sticky-wallets with your own ID for that customer. The first call makes the addresses. Every later call returns the same ones.
  3. 3You show the address, with a QR code, for the coin the customer picks.
  4. 4When the coins arrive, you get deposit.pending. Once the network has confirmed them, you get deposit.confirmed.
  5. 5On deposit.confirmed, you credit the customer named in sticky_wallet.reference.
curl
curl https://cryptomerchant.online/api/v1/sticky-wallets \
  -H "Authorization: Bearer $CM_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reference": "user-1042" }'

The response lists one entry per network, each with the address and the coins it takes. The call is safe to repeat, so you don't have to store the addresses. Call it from your server whenever the deposit page opens, and take the customer ID from your own session, never from the browser.

Pick your reference with care. It can be 1 to 120 letters, digits or . _ : @ + -, and each reference has one wallet for good. An internal user ID is a better choice than an email address, which a customer may change.

When should I credit the customer?

Credit on deposit.confirmed only. The other two events are for your interface.

EventWhat it meansWhat to do
deposit.pendingSeen on the network, waiting for confirmationsShow "deposit on its way". Don't credit it
deposit.confirmedFinal, and added to your Crypto Merchant balanceCredit the customer once
deposit.droppedSeen, but it never confirmedRemove the "on its way" message. Nothing to reverse, because you didn't credit it

The wait depends on the network. These are the times from our network status page on 6 November 2026:

NetworkConfirmationsTypical wait
BNB Smart Chain, Base, Arbitrum, Tron, Solana15–32 (varies by network)About 1 minute
Ethereum, Polygon12 on Ethereum, 64 on PolygonAbout 2 minutes
Litecoin6About 15 minutes
Bitcoin2About 20 minutes

How do I avoid crediting a deposit twice?

Make the deposit ID a unique key in your own ledger, and write the credit and the balance change in one database transaction. Webhooks are delivered at least once, so the same event can arrive twice. A customer balance is the worst place for a duplicate, because it turns directly into spendable value.

PostgreSQL
CREATE TABLE account_credits (
  deposit_id  text PRIMARY KEY,
  customer_id text NOT NULL,
  coin        text NOT NULL,
  amount      numeric NOT NULL,
  tx_hash     text NOT NULL,
  credited_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE balances (
  customer_id text NOT NULL,
  coin        text NOT NULL,
  amount      numeric NOT NULL DEFAULT 0,
  PRIMARY KEY (customer_id, coin)
);
credit.js
const API = "https://cryptomerchant.online/api/v1";

// Runs for each saved deposit.confirmed event, and from the catch-up job. Safe to run twice.
export async function creditDeposit(deposit, { db }) {
  if (deposit.status !== "confirmed" || !deposit.sticky_wallet) return false;
  if (deposit.net_amount === null) return false; // not in your balance yet: the next run picks it up

  const client = await db.connect();
  try {
    await client.query("BEGIN");
    // The primary key on deposit_id means a repeat inserts nothing and credits nothing.
    const { rowCount } = await client.query(
      `INSERT INTO account_credits (deposit_id, customer_id, coin, amount, tx_hash)
       VALUES ($1, $2, $3, $4, $5) ON CONFLICT (deposit_id) DO NOTHING`,
      [deposit.id, deposit.sticky_wallet.reference, deposit.coin.id, deposit.net_amount, deposit.tx_hash],
    );
    if (rowCount === 1) {
      await client.query(
        `INSERT INTO balances (customer_id, coin, amount) VALUES ($1, $2, $3)
         ON CONFLICT (customer_id, coin) DO UPDATE SET amount = balances.amount + EXCLUDED.amount`,
        [deposit.sticky_wallet.reference, deposit.coin.id, deposit.net_amount],
      );
    }
    await client.query("COMMIT");
    return rowCount === 1;
  } catch (err) {
    await client.query("ROLLBACK");
    throw err; // the job tries again later
  } finally {
    client.release();
  }
}

Check the webhook's signature and save the event before you run this. The webhook retries and reconciliation guide shows how. Store amounts as numeric or as strings, as the API sends them. Floating-point numbers will drift.

What if my server misses a webhook?

Crypto Merchant retries a failed webhook up to eight times over about two days. For longer outages, list your confirmed deposits on a schedule and credit any you haven't handled. The unique key makes this safe.

catch-up.js
// Scheduled catch-up: walks confirmed deposits, newest first, and credits any you missed.
export async function catchUp({ db, fetchFn = fetch, maxPages = 50 }) {
  let after = null;
  let credited = 0;
  for (let page = 0; page < maxPages; page++) {
    const query = new URLSearchParams({ status: "confirmed", limit: "100" });
    if (after) query.set("starting_after", after);
    const res = await fetchFn(`${API}/deposits?${query}`, {
      headers: { Authorization: `Bearer ${process.env.CM_SECRET_KEY}` },
    });
    if (!res.ok) throw new Error(`Deposit list failed: ${res.status}`);
    const list = await res.json();
    for (const deposit of list.data) {
      if (await creditDeposit(deposit, { db })) credited++;
    }
    if (!list.has_more || list.data.length === 0) break;
    after = list.data[list.data.length - 1].id;
  }
  return credited;
}

The default of 50 pages covers your latest 5,000 confirmed deposits. Set maxPages to match how far back you could have missed something. A deposit in the list can show as confirmed just before it's added to your balance. Its net_amount is null until then, so the code skips it and a later run or the webhook credits it.

Should I credit in the coin or in dollars?

That depends on what your balance is for. Each deposit gives you the amount in the coin and its value in USD when it arrived.

Your balance is inCreditWatch out for
The coin itselfnet_amount, per coin.id such as USDT.TRONKeep one balance per coin and network, or decide that USDT on different networks counts as the same
USD or creditsamount_usd × (net_amount ÷ amount)Decide the rule once and show it. The value is fixed when the deposit arrives, so you carry any price move until you pay out or swap

If you don't want to carry price movement, accept stablecoins only, or choose a holding coin so that what you receive is swapped automatically. Swaps use the live market rate, which includes our 1% service margin, plus a gas fee.

What does a top-up cost?

Crypto Merchant's standard fee is 1% of each deposit, with no setup or monthly fee. At 1%, a 250 USDT deposit costs 2.5 USDT, and 247.5 USDT is added to your balance. The fee field on each deposit shows what was charged. You choose who bears it: credit net_amount to pass it to the customer, or amount to cover it yourself.

Moving coins carries a gas fee too, from the gas balance or the coins being moved. It's higher on some networks than others, as today's network fees show, so small deposits on an expensive network leave you less. Showing a minimum top-up per network on your deposit page is a sensible protection. See pricing for the full list.

What do customers need to be told?

Show each address together with the coins listed for it, and say this plainly on the deposit page:

  • Send only the coins listed. Coins on another network, or tokens that aren't listed, are not credited automatically. The customer has to contact you, and you contact support.
  • One address can serve several networks. The evm address is the same on Ethereum, BNB Smart Chain, Polygon, Base and Arbitrum. Each network still appears as its own entry in the coins list.
  • The address can be saved. It never changes, so regular customers can save it. That is a large part of the appeal.
  • Exchanges deduct their own fee. A customer who withdraws 50 USDT from an exchange may deliver less. You credit what arrives.

XRP and TON aren't available for sticky wallets, because every new address on those networks costs money to open. If pending_networks lists a network, its address is still being prepared. Call again a few minutes later.

One more point for privacy-minded customers: a permanent address links that customer's deposits to each other on a public blockchain. For most top-up products that's an acceptable trade-off, but it's worth knowing.

What do I have to build myself?

The customer's balance and everything that uses it. Crypto Merchant holds the coins in custody and tells you who sent what. It doesn't keep a balance per customer for you to spend from.

PartCrypto MerchantYou
Deposit addresses and watching the networksYes
Signed webhooks and the deposit listYes
Keys and custody of the coinsYes: Crypto Merchant is custodial
Customer balances and spendingYes
Deposit page, minimums and messagesYes
Withdrawals back to your customersYes
Tax, licensing and reporting for your businessYes

Withdrawals deserve thought before you launch. The API has no endpoint for sending coins to your customers, and payouts go to addresses an Admin has saved in the portal. So if customers can withdraw, you pay them from your own wallet or by hand. Refund links belong to paid invoices, so a sticky wallet deposit doesn't have one.

Holding money for customers can be regulated. In many countries, keeping balances that people can withdraw or spend with third parties is treated differently from selling your own prepaid service. Ask a lawyer before you offer anything beyond credits for your own product. This article is not legal advice. The Terms prohibit illegal and sanctioned uses, and the eligibility page has the full list.

How do I get sticky wallets?

Choose "Customer wallets" when you sign up. If you already have an account, ask support to switch them on. No identity documents are needed to create an account. Then:

  1. 1Finish setup and choose the coins you accept. Each customer gets an address on those coins' networks.
  2. 2Create a secret key under Developers, and keep it on your server.
  3. 3Add a webhook endpoint with the three deposit events, and keep its signing secret.

Until sticky wallets are on and setup is finished, these endpoints answer 403 sticky_wallets_unavailable. The full reference is in the sticky wallet docs. Crypto Merchant is custodial; see custodial vs non-custodial. For taking payments without a website, see crypto payment links.

Frequently asked questions

Can a customer top up any amount?
Yes. A sticky wallet takes whatever is sent, so there's no underpaid or overpaid state. If you want a minimum, set and display it on your own deposit page.
Do the addresses expire?
No. A customer's addresses never change and aren't given to anyone else.
Can I show the customer their deposit before it confirms?
Yes, using deposit.pending. Show it as "on its way" and keep it out of the spendable balance until deposit.confirmed.
What if a customer sends the wrong coin?
Coins on another network, or tokens that aren't listed for that address, aren't credited automatically. Contact support with the transaction hash.
Can I use sticky wallets without coding?
Not fully. You can create customer wallets in the portal and see their deposits there, but crediting a balance automatically needs your own server. If you only want to take payments without a website, use payment links.
Is Crypto Merchant custodial?
Yes. Crypto Merchant holds the keys to the wallets where deposits arrive and collect, until you pay out to your own addresses.

Start accepting crypto today

Free setup, no documents to sign up, 1% per paid invoice, and payouts whenever you want.