An ERC-20 transfer call takes two arguments, a recipient and an amount, and nothing else. There is no reference field, so a USDC payment on Ethereum arrives with no account number and no invoice ID, just a sender address that may well belong to an exchange's omnibus wallet.
Every team that accepts stablecoins has to decide how to work out who paid. The guides that rank for this question mostly say "use a unique address per customer" and stop there. None of them compares the options chain by chain, and none covers the parts that go wrong: sweep gas, memo fees, exact-amount matching and address poisoning. This guide does.
How Do You Match Incoming USDC Payments to Customers?
There are four ways to attribute an incoming stablecoin payment. Each one puts the identifier in a different place.
| Method | Where the identifier lives | Chains | Main failure |
|---|---|---|---|
| Deposit address per customer or invoice | The to address | All | Sweep cost, address management |
| Memo, tag or note | A field on the transaction | Stellar, XRPL, Tron, Solana, Algorand | Sender omits it |
| Reference key | An extra read-only account on the transaction | Solana (Solana Pay) | Wallet support |
| Unique amount | The amount itself | All | Fees and rounding change the amount |
The rest of this guide covers how each one behaves on real chains and which to pick.
Why ERC-20 Transfers Have No Memo Field
The EIP-20 specification defines the call and the event like this:
function transfer(address _to, uint256 _value) public returns (bool success)
event Transfer(address indexed _from, address indexed _to, uint256 _value)
There is no bytes data parameter. The Transfer log is the only thing your indexer sees, and it carries three values. Any reference you want has to be encoded in one of those three.
The payment-request URI standard does not help. EIP-681 (Final since 2017) lets you encode ethereum:, but it has no reference parameter. The amount is also only a suggestion, and the user's wallet can change it.
There is a proposal to fix this. ERC-7699, the "ERC-20 Transfer Reference Extension", adds transfer(address to, uint256 amount, bytes calldata transferReference) and a TransferReference(bytes32 indexed loggedReference) event emitted before Transfer. Its motivation section cites MT103 remittance information and ISO 20022 RmtInf by name. It has been a Draft since April 2024. Neither USDC nor USDT implements it, and the existing token contracts would need an upgrade to do so.
So on EVM chains the honest answer is that you attribute payments by address, or by amount, or not at all.
Per-Customer Deposit Addresses on Ethereum: HD Wallets vs CREATE2
Giving each customer (or each invoice) their own receiving address is the default on EVM chains. There are two ways to generate those addresses, and they differ mostly in what sweeping costs.
HD wallets (BIP-32/BIP-44). You derive child keys from one seed, one index per customer. Generating addresses costs nothing and can happen offline. The problem shows up when you consolidate. Each derived address holds USDC but no ETH, so before you can move the funds you have to send it gas. That is two transactions per sweep, plus a dust balance of ETH left behind in every address. A plain USDC transfer uses roughly 40,000 to 47,000 gas according to receipts on Etherscan. Moving the funds in is the cheap part. The cost comes from topping up gas thousands of times. CREATE2 counterfactual addresses. You compute the address a contract would have if deployed with a given salt, and hand that out. Funds can land there before any code exists. Relay's deposit address design is a clean public example: EIP-1167 minimal proxies, saltkeccak256(orderId, depositor), and a sweep call that deploys the proxy and forwards the balance in one atomic transaction. If the sweep fails, the funds stay where they are. You pay deployment gas once per address, but you never fund the address with ETH, and you never hold a private key for it.
BitGo's forwarder contracts follow the same pattern. Their older v0 forwarder deploy carries a 102,000 gas limit, and newer versions deploy on first receipt.
EIP-7702, now Final, changes the HD wallet maths. An EOA can delegate to contract code, which means a sponsor can pay gas for it. My reading is that this lets you sweep HD-derived addresses from a central gas account without pre-funding each one. The spec itself warns that a delegating account can sweep its own assets and leave a relayer unpaid, so keep the sponsor logic on your side.On L2s such as Base, gas is cheap enough that the HD wallet top-up dance is tolerable. On Ethereum mainnet I would not run it beyond a few hundred addresses.
Solana Pay Reference Keys vs the Memo Program
Solana gives you two attribution tools that EVM chains lack, and the better one is not the memo.
Reference keys. The Solana Pay transfer request spec lets the merchant add one or morereference values, each a base58-encoded 32-byte public key. The wallet must include them "in the order provided as read-only, non-signer keys" on the transfer instruction. You generate a fresh keypair per payment, throw away the private key, and poll getSignaturesForAddress(reference) until a signature appears. Then you fetch the transaction and check the amount and mint.
This is the cleanest attribution primitive on any major chain. The identifier is unique per payment, it is indexed by every RPC node, and it costs the payer nothing extra.
The Memo program. The SPL Memo program (MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr) records a UTF-8 string, up to 566 bytes for an unsigned instruction. Solana Pay puts it second-to-last, directly before the transfer. It works, but the payer has to type or paste it correctly, and it is public, so never put a customer identifier in plain text.
Per-customer token accounts. You can also give each customer their own wallet. On Solana the token balance lives in an Associated Token Account derived from [wallet, TOKEN_PROGRAM_ID, mint], and creating one costs 0.00203928 SOL in rent for its 165 bytes. Anyone can create it, and you get the rent back when you close it. That is cheap, but it is still per address, and you still have to sweep.
For checkout flows on Solana, use reference keys. Keep per-customer addresses for standing deposit accounts, where the customer pays from an exchange that will not support Solana Pay.
Memos on Tron, Stellar and XRP Ledger: Fees and Required Tags
The memo-native chains each have their own rules. These are the ones that bite.
| Chain | Field | Limit | Enforcement |
|---|---|---|---|
| Stellar | MEMO_TEXT / MEMO_ID / MEMO_HASH | 28 bytes / uint64 / 32 bytes | SEP-29 config.memo_required = "1" |
| XRP Ledger | Destination tag | uint32 | RequireDest flag rejects untagged payments at ledger level |
| Tron | Transaction memo | n/a | 1 TRX fee per memo transaction since Dec 2022 |
| Solana | Memo program | 566 bytes (unsigned) | None |
| Algorand | Note | ~1 KB | None |
config.memo_required, and compliant wallets refuse to submit a payment without one. Stellar now steers pooled accounts towards muxed accounts, which bake a 64-bit ID into the address itself. That turns a memo into an address, which tells you which model Stellar's own developers trust.
XRP Ledger goes further. With RequireDest set, the ledger itself rejects a payment with no destination tag. XRPL's docs also recommend non-sequential, hash-derived tags, because ledger data is public and sequential tags leak your customer count.
The memo model's real cost is human. Kraken's deposit recovery page says a missing or wrong memo "can prevent your deposit from being credited", and recovery goes through a support ticket with the TxID. If the chain can enforce the tag, as XRPL can, turn enforcement on.
Unique Amounts: How Stripe Deposit Mode Matches Stablecoin Payments
The fourth method is to make the amount itself the identifier. Stripe's deposit mode for stablecoin payments, a preview behind Stripe-Version: 2026-03-25.preview, combines this with a deposit address:
POST /v1/payment_intents
payment_method_types[]=crypto
payment_method_options[crypto][mode]=deposit
payment_method_options[crypto][deposit_options][networks][]=base
payment_method_options[crypto][deposit_options][networks][]=solana
The response carries next_action.crypto_display_details.deposit_addresses.{network}.address. The amount has to match to the minor unit: a 1.01 USD charge means exactly 1.010000 USDC, or 1,010,000 base units. Stripe states that it cannot automatically match or return overpayments, underpayments, the wrong token or network, or funds that arrive after the PaymentIntent expires.
That is the trade-off of exact-amount matching in one sentence. It works with any wallet, but it breaks on every exchange withdrawal that deducts its fee from the amount sent. If you use amounts alone, without a dedicated address, you are also capping concurrency. You can only have one open invoice for 100.00 at a time, so you end up adding sub-cent jitter (100.000137) and hoping nobody rounds it. For the decimals involved, see ISO 4217 minor units. Native USDC uses six decimals, but bridged and other tokens can differ, so read decimals() per token per chain.
Stripe vs Bridge vs Circle: Deposit Address APIs Compared
If you would rather not run address generation yourself, three provider APIs show the main design choices.
| Provider | Endpoint | Address scope | Attribution model |
|---|---|---|---|
| Stripe (preview) | POST /v1/payment_intents with mode=deposit | Per payment | Address plus exact amount, with an expiry |
| Bridge | POST /v0/customers/{id}/liquidation_addresses | Per customer, permanent | Address mapped to one fiat or crypto destination |
| Circle Mint | POST /v1/businessAccount/wallets/addresses/deposit | Per business wallet, per chain | Address plus addressTag where the chain needs one |
currency, chain and a destination (external_account_id plus destination_payment_rail, which defaults to ACH, or a destination_address for crypto). Anything sent there is converted and pushed out. Attribution is solved by design, because the address is the routing instruction. It also takes destination_sepa_reference and destination_wire_message, so the fiat leg carries a reference even though the crypto leg could not. For Stellar sources it needs a memo, or offers a memoless_address.
Circle Mint is a treasury tool, not a customer-attribution one. Its docs say Circle "may reuse addresses on blockchains that support reuse" and recommend one address per chain. If you build per-customer attribution on top of a Circle Mint deposit address, you are back to amounts.
Coinbase's Onchain Payment Protocol takes a different route. The payer calls a contract with a signed TransferIntent carrying a unique id, recipient, amount and deadline, and the contract emits a Transferred event with that id. This is ERC-7699's idea built at the application layer: the reference travels in a contract call instead of a token transfer. The catch is that the payer's wallet must call the contract, so a plain withdrawal from an exchange cannot use it.
Which Stablecoin Attribution Method Should You Use in 2026?
My position, for anyone building on USDC or USDT today:
- EVM chains: per-customer CREATE2 deposit addresses, swept by a contract call. HD wallets only on L2s, or with EIP-7702 sponsorship.
- Solana checkout: Solana Pay reference keys, one per payment.
- Tron, Stellar, XRPL standing accounts: per-customer addresses on Tron (the memo fee and spam history make memos a poor fit there). On Stellar use muxed accounts, and on XRPL use tags with
RequireDeston. - Never rely on exact amount alone for a customer-funded flow. Use it as a check on top of an address, as Stripe does.
One security point applies to every address-based design. The USENIX Security 2025 address poisoning study counted 270 million attacks against 17 million victims on Ethereum and BSC over two years, with at least $83.8 million lost. The attack works because ERC-20 requires zero-value transfers to emit a normal Transfer event, so an attacker can plant a lookalike address in a victim's history. If you show customers a deposit address, they will copy it from their own transaction history next time. Rotate per-invoice addresses, or show a QR code and checksum on every payment, never just "your address".
What This Means for Payment Engineers
1. Index Transfer events by recipient, not by sender. Sender addresses on exchange withdrawals belong to omnibus wallets and identify nobody. Store the sender for refunds and Travel Rule purposes, but never key attribution on it. The Travel Rule protocols guide covers what you owe the counterparty VASP.
2. Model attribution as a strategy per chain. Your ledger should store (chain, token, method, identifier), where method is one of address, memo, reference, amount. A single "deposit address" column will not survive adding Stellar or Solana Pay.
3. Budget the sweep before choosing HD wallets. Multiply your expected address count by two transactions per consolidation on your target chain. If the number is uncomfortable, use CREATE2 or a provider.
4. Route unmatched funds to a suspense account with an SLA. Wrong amount, missing memo, wrong network: these will happen weekly. Stripe will not return them automatically, and neither will you unless you build it. The crypto-to-fiat off-ramp guide covers what happens after that.
5. Make memos mandatory where the chain allows. XRPL RequireDest, Stellar SEP-29. Everywhere else, prefer an address.
6. Make the attribution step idempotent on (chain, tx_hash, log_index). One transaction can carry several transfers, and a reorg can replay one. The idempotency keys guide covers the storage pattern.
Card rails solved this with a merchant ID and an order reference decades ago. Stablecoins put the reference back in your hands, and on most chains there is nowhere in the transaction to put it. I am Tom Wang, and in my experience the teams that struggle with stablecoin reconciliation chose an attribution model by accident, usually a single shared address and a spreadsheet. Pick the model per chain before you go live, not after the first unmatched deposit.