USDC on Base and USDC on Ethereum are the same dollar. They are not the same payment. The gap between them is settlement-rail mechanics, and it shows up on every invoice.
It is the same token issuer, the same dollar peg, the same three-letter ticker. A merchant looking at "accept USDC" treats it as one decision. The payer doesn't. The payer has USDC sitting in one wallet on one network, and the cost, the wait, and the muscle memory of moving it are entirely a function of which network that is. Ethereum mainnet and Base are both real homes for USDC, and they pull in opposite directions on the variables that actually decide whether a payment feels good to send.
This is the version of the conversation we have internally before we tell a merchant which rail to default to.
The fee gap is a network property, not a price
The most visible difference between the two is what it costs to move the coins. On Ethereum mainnet, a USDC transfer pays Ethereum mainnet gas — and that is the highest-cost rail of the three networks we accept USDC on. Base is an Ethereum L2 built for exactly this: the same ERC-20 USDC, settled for a fraction of mainnet cost.
We are deliberately not putting a number on that. The number moves with network conditions, and a blog post that quotes a gas figure is wrong by the time it's published. What matters is the shape: on mainnet the transfer cost is a real line item that a payer feels on a small payment, and on Base it recedes into rounding noise. That shape is stable even when the digits aren't.
The consequence is a clean rule of thumb:
- Small and everyday dollar payments lean Base. A $19 or $49 charge where mainnet gas is a meaningful slice of the total — steer the payer to Base if they hold there.
- Large payments tolerate Ethereum. When the amount dwarfs the fee, mainnet's cost stops mattering, and you get the deepest, most canonical USDC liquidity in return.
Confirmation time is the second axis
Fee is loud. Confirmation behavior is the quieter variable that decides how a payment feels, and it's where the per-chain detail lives.
Ethereum mainnet finalizes on its own schedule, and we confirm a USDC deposit against Ethereum's threshold before an invoice flips to paid. Base, as an L2, has its own block cadence and its own threshold. Neither is "instant," and we don't pretend either is — what we do is set confirmation thresholds per chain, because finality means something different on mainnet than it does on an L2. A USDC payment is marked paid when the network it arrived on says it's final enough to act on, not when a single global rule says so.
This is also where reorg-awareness earns its keep. If a block carrying a payment is unwound, that's reflected rather than silently kept as settled. The threshold and the reorg handling are the reason you can release goods against a confirmed USDC payment without keeping a block explorer open in another tab.
Payer habits decide more than the spreadsheet
Here is the part that a fee-and-latency table misses: the payer usually isn't optimizing. They have USDC in one place, and the network that place lives on is the network they'll pay from. An EVM-native treasury that custodies on Ethereum mainnet will reach for mainnet because that's where the balance is, and a $12 transfer cost on a six-figure settlement is beneath their notice. A buyer who got their USDC on Base, or bridged it there to live cheaply, will pay from Base and would be annoyed to be pushed onto mainnet for a $40 charge.
So "which rail is better" is the wrong question. The right one is which rail is the payer already standing on. Ethereum is the canonical, institutional default; Base is the everyday, low-cost default. Most of your payers have already made the choice for you — your job is to not override it.
(If you want the chain-by-chain breakdown across every asset we settle, the networks page is the reference. For USDC specifically, the USDC asset page covers all three rails, including Solana, side by side.)
The fiat anchor is how you stop choosing
Everything above is a real tradeoff, and the temptation is to resolve it by picking one rail and calling it your USDC strategy. Don't. The fix is to not bill in USDC at all.
You don't create a "USDC on Base invoice." You create a dollar invoice and let the payer settle it in the network they hold. A fiat-anchored invoice carries an amount_fiat and a fiat_currency, the rate locks at activation, and from that point the payable token amount is fixed for the life of the invoice. Whether the customer pays USDC on Base, USDC on Ethereum, or USDC on Solana, you reconcile against the same USD figure you billed. The rail is the payer's choice; the dollar is yours.
A minimal create call is one authenticated request:
curl -X POST https://api.thehalfin.com/api/v1/invoices \
-H "X-API-Key: $HALFIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_fiat": "49.00",
"fiat_currency": "USD",
"deferred": true,
"description": "Pro plan — March",
"idempotency_key": "00000000-0000-4000-8000-000000000001"
}'
A few things worth being precise about, because they trip people up:
- The amount is a string (
"49.00"), not a float. Monetary values are strings end to end. idempotency_keyis a field in the JSON body, not a header. The only headers you send areX-API-KeyandContent-Type. Re-sending the same key returns the same invoice instead of creating a second one.deferred: trueis what makes the rate lock at activation rather than at create time — the payable token figure is set when the customer is actually ready to pay.
The invoice that comes back is payable on any USDC rail you support. The payer picks Base or Ethereum at the checkout; you never had to.
Knowing the payment landed
You track a USDC payment the same way regardless of which rail it arrived on. State changes are observable in the dashboard, over the REST API, and through signed webhooks. A verified invoice.paid event means the USDC reached the per-chain confirmation threshold on whichever network it came in on and the dollar amount settled to your balance. invoice.underpaid and invoice.overpaid tell you a payment arrived but didn't match the expected figure — common enough that you should handle them deliberately rather than treating them as failures.
Verify the HMAC signature over the raw bytes before you act on any of these. That discipline is the same on Base as on Ethereum; the rail changes, the contract doesn't. (We've written about why you verify before acting at length.)
The operating rule
Base and Ethereum are not competitors for your USDC business. They're two doors into the same room, and your payers have already picked their door. Bill in dollars, lock the rate at activation, offer both rails on the invoice, and let the customer settle on the network they were always going to use. The fee and confirmation differences are real — they're just not yours to resolve.
If you're wiring this up for the first time, the how to accept USDC guide walks the full integration end to end.