The problem with an invoice for every top-up
Invoicing is the right primitive for a one-off purchase: a fixed amount, a locked rate, an expiry window. But account funding is a different shape. A customer who tops up a wallet, a trading account, or a prepaid balance does it repeatedly and on their own schedule. Forcing an invoice into that flow means a new payment object, a new address, and a new expiry every single time — and a customer who pays a few minutes after the window closes ends up on a stale address.
It also breaks the one habit that makes recurring funding painless. A customer who can save a single address to their wallet's address book, set up a standing transfer, or paste the same string from memory will fund again with almost no friction. An address that changes per payment removes that affordance and turns every top-up into a fresh, error-prone copy-paste.
Static deposit addresses solve the funding case directly. halfin issues a persistent receive address tied to a customer account on the network you choose. The customer reuses it indefinitely; halfin watches it, credits each incoming deposit to your merchant balance, and notifies your backend. There is no amount to fix in advance and nothing to expire.
How a static deposit address works
You request a deposit address for a given customer and network. halfin returns an address that is now permanently associated with that customer in your account. You store the address against your own user record and show it on your funding screen — usually as text plus a QR code so the customer can scan it from a mobile wallet.
From that point the address is monitored continuously. When a deposit arrives, halfin detects it on-chain, applies the same reorg-aware crediting and per-chain confirmation thresholds used everywhere else on the platform, and moves the funds into your merchant balance. Because there is no pre-declared amount, halfin credits whatever the customer actually sends — there is no underpaid or overpaid state to reconcile the way an invoice has.
The customer can deposit to that address any number of times. Each deposit is an independent event with its own transaction hash, confirmation count, and credited amount. You correlate deposits back to the customer through the address you already stored, which keeps the mapping unambiguous: one address, one customer, many deposits.
Because the binding from address to customer is made once, at issuance, the rest of your accounting falls out of it. You do not parse a memo, match an amount, or guess which user a deposit belongs to — the receiving address is the identifier. A returning customer who sends a second, third, or tenth top-up to the address they saved is attributed by exactly the same lookup as the first, so the cost of supporting recurring funding does not grow with the number of deposits.
- Persistent — issued once per customer + network, reused for the life of the account.
- Amount-free — halfin credits the actual amount received; no fixed total, no expiry.
- Reorg-aware — credited only after the chain's confirmation threshold, like every other deposit path.
- Webhook-driven — your backend learns about each deposit on detection and on confirmation.
Invoices vs. static deposit addresses
Both primitives accept crypto into your balance. The difference is whether the payment is a discrete, priced event or an open, repeatable funding channel. Use invoicing when you need a customer to pay a specific amount once; use a static deposit address when the same customer funds an account again and again.
| Property | Invoice | Static deposit address |
|---|---|---|
| Lifetime | Single payment, then expires | Persistent across many deposits |
| Amount | Fixed and quoted at activation | Whatever the customer sends |
| Rate lock | Locked at activation, expires | Not applicable — no quote |
| Expiry | Yes (TTL) | None |
| Best fit | Checkout, one-off sale, contract pre-pay | Account top-ups, recurring funding |
| Customer habit | New link each time | Save once, reuse forever |
Supported networks and assets
A deposit address is network-specific. A Tron address receives TRC-20 deposits; an Ethereum address receives ETH and ERC-20 tokens; a Solana address receives SOL and SPL tokens. Issue the customer an address on the network they prefer to send from, and let high-fee networks be a deliberate choice rather than a default.
For stablecoin funding this matters in practice. The same logical balance — say USDT — is reachable on Tron, Ethereum, or Solana, and the cost and speed of a deposit differ sharply between them. Many merchants default returning customers to a low-fee network for top-ups while still supporting the others for customers who already hold funds elsewhere.
| Network | Native asset | Stablecoins on this network |
|---|---|---|
| Bitcoin | BTC | — |
| Ethereum | ETH | USDT (ERC-20), USDC (ERC-20) |
| Base | ETH | USDC |
| Arbitrum | ETH | — |
| Polygon | Native Polygon token | — |
| BNB Smart Chain | BNB | — |
| Tron | TRX | USDT (TRC-20) |
| XRP Ledger | XRP | — |
| Solana | SOL | USDT (SPL), USDC (SPL) |
Deposits credit your balance and notify your backend
A static deposit address is only useful if your system reacts when money arrives. halfin emits signed webhook events as each deposit moves through its lifecycle: one when the deposit is first detected on-chain and one when it reaches the network's confirmation threshold and is credited. Your backend uses these to update the customer's funded balance, enable a paid feature, or trigger fulfilment.
Treat the webhook as a trigger, never as trusted data on its own. Every event carries an HMAC signature; verify that signature against your endpoint secret before you take any business action, and only act on the confirmed event when the action moves real value. The detection event is good for an optimistic "deposit seen" state in your UI; the confirmation event is the one that should credit the customer's account in your ledger.
The two events also give you a clean way to handle the gap between "seen" and "final." Showing a pending state on the detection event keeps a funding customer informed without committing value, and waiting for the confirmation event before you credit their account means a deposit that a reorg later unwinds never leaves a phantom balance behind. The detection-then-confirmation pair is the same on every supported chain; only the threshold that defines confirmation differs per network.
Once funds are credited to your merchant balance, the rest of the platform is available to them. You can convert a deposited asset into another balance for treasury reasons, or pay out from the balance later — the deposit address is the funding edge of the same account, not a separate silo.
- Detection event — the deposit has been seen on-chain (optimistic UI only).
- Confirmation event — the deposit has cleared the threshold and is credited (act here).
- Verify the HMAC signature before any business action; re-fetch state if you need certainty.
- Credited funds land in your merchant balance, ready for conversion or payout.
# Request a persistent deposit address for one of your customers.
curl -X POST https://api.thehalfin.com/api/v1/addresses \
-H "X-API-Key: $HALFIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"network": "tron",
"asset": "usdt",
"reference": "customer:84213",
"idempotency_key": "cust_84213-tron-usdt"
}'
# The address you store and show the customer is reused for every top-up.
# Each deposit later arrives as a signed webhook your backend verifies.Operational and compliance notes
Map one deposit address to one customer and keep that mapping in your own system. Because the address is the only thing tying an inbound deposit back to a user, reusing a single address across multiple customers makes attribution impossible. Store the address against your user record at issuance and treat it as part of that customer's profile.
The same KYB onboarding and AML awareness that govern the rest of your halfin account apply to funds arriving on a deposit address. Persistent addresses make a customer's deposit history easy to follow over time, which supports source-of-funds review and travel-rule processes as a matter of operational hygiene. Keep counterparty screening on the inbound side where your policy requires it — halfin gives you the records, your compliance process makes the decisions.
An address is a receive endpoint, not a vault. halfin's custody posture here is the same as elsewhere on the platform: control comes from scoped API keys, operator permissions, and a full audit trail of every deposit and balance movement — not from any guarantee beyond those mechanisms.