Blockchain · Smart contracts · DeFi · 18 min
Where Your USDC Actually Goes
The confusing moment I used to see “transaction submitted” and think the money had moved. Then the user’s balance stayed exactly where it was.
That belief was reasonable: in an ordinary app, a successful API response often means the job is done. On a blockchain, “we accepted the request” and “the network changed” are two different events.
We’ll follow one 20 USDC withdrawal from a Fasset-style app, through Fireblocks and Ethereum, then use the same path to open the door to DeFi.
Send the 20 USDCOne tap, several truths
The API said yes. The blockchain said nothing yet.
Press Withdraw. Watch the same transfer mean something different at each stop.
- Send
- 20 USDC
- To
0xAda- Network
- Ethereum
- 01Fasset APIValidates the request
- 02Wallet jobSaves intent and queues work
- 03FireblocksChecks policy and signs
- 04EthereumReceives the signed instruction
- 05USDC contractChanges token balances
- 06BlockOrders and records the change
- 07Fireblocks webhookReports the new status
- 08Fasset databaseReconciles its local view
The app balance is only a local view until evidence returns.
The first response means, “Your request is valid and we accepted the work.” It does not mean Ethereum has seen, executed, or recorded anything.
That is why a good backend stores a local status such as SUBMITTED, does the slow work outside the request, then waits for stronger evidence.
POST /withdraw
202 Accepted
status: "SUBMITTED"
“Your transfer has started” is honest. Arrival needs a later network status, not optimism from the first HTTP response.
Keep thisAn API response confirms backend work. A block confirms blockchain work.
But what is the blockchain changing if the balance also exists in our database?
The number that does not belong to us
A balance is not a number in your database.
Try the tempting shortcut. Then make the network accept the real change.
All three nodes agree: Vault 100, Ada 5.
Your database can say anything. Ethereum nodes only accept a valid signed instruction, then each node computes the same next state.
That shared state is the useful heart of a blockchain: many machines agree on the ordered history and the current result. Your backend keeps a faster local copy for the product.
Where do blocks fit?
Transactions arrive at different machines in different orders. A block gives the network one agreed batch and order, while its link to the previous block makes old history difficult to rewrite unnoticed.
On Ethereum, validators propose blocks and other nodes re-execute their transactions. If the resulting state does not match, the block is invalid.
Keep thisYour database remembers what the product believes. The chain decides what the asset can actually do.
The nodes demand a signature. Who is allowed to make it?
The authority room
The vault holds authority, not a pile of coins.
Move the request through the signing room. The full private key never appears in the app or ordinary service code.
0xAda
This is only data. Anyone can write it.
The intent is waiting outside the vault.
A wallet address is public, like an account number. The private signing authority is the thing that must stay protected.
In this architecture, a Fireblocks vault account groups asset wallets and addresses. Policies decide which transactions may be signed; MPC spreads signing work across separate key shares so one exposed secret is not the whole key.
Two keys that are easy to confuse
Your backend’s Fireblocks API secret proves which software called Fireblocks. The blockchain key proves which on-chain account authorized the transaction.
They protect different doors. Leaking an API credential is serious, but it is not the same object as an Ethereum private key.
Keep thisThe address identifies an account. The signature authorizes a state change. The vault protects how that signature is made.
We signed a USDC transfer. Why does Ethereum need a smart contract for that?
The coin inside the machine
USDC lives inside a program.
Switch the asset below. An ETH transfer and a USDC transfer look similar in the app, but the network executes different work.
to
0xAda
Debit the vault’s ETH balance. Credit Ada’s ETH balance.
to: "0xAda"
value: parseEther("0.2")
data: "0x"
No contract call is needed to move Ethereum’s native asset.
ETH is native to Ethereum, so the protocol tracks its balance directly. USDC is a token contract with a table that roughly says, “this address owns this many units.”
Sending USDC means calling that contract’s transfer function. The contract checks the rules, subtracts 20 from the vault’s token balance, adds 20 to Ada’s, and emits a log that indexers can find.
The fee is still paid in the network’s native asset. A USDC wallet therefore needs ETH for gas—or infrastructure such as a gas station must fund it.
Wallet UI:
USDC balance = 80
What owns that number?
The wallet and Fireblocks display the balance. The token contract’s state is the source the network executes.
Where the physical analogy stops
No tiny USDC coins travel through a pipe. The network executes a valid instruction and changes agreed numbers.
A wallet “contains crypto” in the everyday sense, but technically it manages addresses, signing authority, and a view of balances recorded elsewhere.
Keep thisNative asset transfer: change protocol balances. Token transfer: call a contract that changes token balances.
The contract ran. Is the withdrawal finished the instant one validator includes it?
The waiting room has stages
Pending is a place, not a failure.
Advance the transfer one step at a time. Notice when it becomes visible, successful, and safe enough for the product.
- 1CreatedBackend intent
- 2SignedAuthorized
- 3BroadcastNetwork sees it
- 4IncludedInside a block
- 5ConfirmingMore history lands
- 6CompletedPolicy threshold met
Only the backend knows about this request.
Created. Only the backend knows about this request.
Broadcast means the network has heard the transaction. Included means a validator put it in a block and execution succeeded.
More blocks make that result harder to replace. Fireblocks can wait for a configured confirmation threshold before calling the transaction COMPLETED.
Why one status name is not enough
A custody platform may expose queued, pending authorization, pending signature, broadcasting, confirming, completed, failed, rejected, blocked, and more. Your product can map those into a smaller public lifecycle without throwing away the detailed internal reason.
Use final states for irreversible product decisions. Treat every non-final webhook as a progress update that may be delivered again.
Keep thisSubmitted is intent; signed is authority; included is execution; completed is your chosen confidence threshold.
The chain now knows. How does the backend learn without asking every second?
Evidence comes home
The webhook closes the loop.
Deliver the same completed transaction twice. A safe backend should settle once and acknowledge both deliveries.
On-chain transfer: COMPLETED
Partner webhook: timed out
The transfer and the notification are separate systems. Persist the truth, then retry the notification with the same transaction identity.
txHash: 0x20ada
status: COMPLETED · amount: 20
Waiting for the completed transaction event.
The transaction hash is the bridge between the public event and your private records. Use it as an idempotency key so webhook retries update the same transaction instead of paying, crediting, or notifying twice.
In a payment flow, settlement may also update an order and its transaction row inside one database transaction. Only after that commit should the service emit live UI events or call the partner.
If the partner endpoint is down, the chain does not roll back. Save a delivery job and retry with backoff.
Keep thisOn-chain finality and off-chain delivery are separate. Reconcile by transaction hash, commit once, retry messages.
So far, the destination was Ada. Change one thing and the transfer becomes DeFi.
The destination has rules
One different address opens DeFi.
A custody API can hide token plumbing. For DeFi, the contract call itself becomes the product action.
0xAda
USDC ends in Ada’s token balance.
operation: "TRANSFER"
beneficiary: "0xAda"
amount: "20 USDC"
Fireblocks builds the required token transaction for Ada.
A DeFi app is a set of smart contracts that hold assets and enforce financial rules. A swap, loan, or liquidity deposit is still a signed transaction—but its destination is a contract and its data names a function plus inputs.
For a USDC TRANSFER, your service names Ada and Fireblocks handles the underlying token call. For a DeFi CONTRACT_CALL, the protocol address and function inputs are the intended action.
In both cases, the custody layer protects and authorizes the signature. The on-chain contract decides what valid execution changes.
Move the slider. You add USDC and remove ETH, so the next ETH becomes more expensive.
This teaching pool ignores gas, slippage limits, routing, and real protocol details.
No employee manually chooses your rate. The pool contract reads its reserves and applies its public rule to every valid call.
Lending uses a different rule: deposit assets into a pool, let borrowers take over-collateralized loans, accrue interest, and liquidate positions that become unsafe. The shape changes; the building blocks do not.
DeFi is “decentralized” at the contract layer, not magically risk-free. Contract bugs, bad price data, governance, liquidity, bridges, stablecoin issuers, and frontend or custody choices can all reintroduce trust.
Approve, then swap?
Many token contracts do not let another contract pull your tokens by default. A separate approve transaction can grant an allowance, then the pool contract uses that allowance during the swap.
That convenience is also risk. Unlimited approvals give a contract continuing power, so production systems inspect destinations, methods, amounts, and policy before signing.
Keep thisDeFi is signed transactions calling financial programs whose shared state lives on the chain.
The chain executes money rules. Why do we still need so many backend services?
Two worlds, one product
The backend keeps both worlds honest.
The chain knows addresses and contract state. Your product knows users, orders, permissions, support cases, and promises.
- Authentication and KYC
- Partner users and orders
- Quotes and fiat accounting
- Local transaction history
- Webhooks, retries, and support
- Vault and asset wallets
- Addresses and destinations
- Policy and approvals
- MPC signing
- Transaction monitoring
- Ordered transactions
- Balances and ownership
- Contract execution
- Logs and receipts
- Confirmations and finality
The blockchain cannot know that 0xAda is a verified Fasset customer paying invoice INV-20. The backend cannot declare that USDC moved merely because a SQL row changed.
The product becomes reliable when every boundary has a durable identity: customer ID, local transaction ID, Fireblocks transaction ID, blockchain hash, order ID, and webhook delivery ID.
Your backend work will live in those joins: validate intent, preserve idempotency, handle partial failure, map statuses, and prove that one event was applied exactly once.
Keep thisThe chain is the asset truth. The backend is the business truth. Transaction identities reconcile them.
Keep one compact map before you open the code.
The screenshot
One transfer, from tap to truth.
Apps request. Signers authorize. Networks order. Contracts execute. Backends reconcile.
When you open the Fasset services, trace one transaction instead of reading folders alphabetically. Start at the controller, follow the queued job, inspect the Fireblocks request, then find the webhook that turns an external status into local state.
After that path feels boring, read the deposit direction, partner notification retries, balance sweeps, and contract-call policy. You will be reading a system you understand—not memorizing names in the dark.
Your first backend code-reading route
- Request boundary: What input is trusted, validated, and authenticated?
- Local identity: Which ID makes retries safe before any external call?
- Queue boundary: What can fail after the HTTP response?
- Custody call: Which vault, asset, destination, amount, and operation are sent?
- Status map: Which external states are non-final, successful, or terminal?
- Webhook guard: Are the original bytes and signature verified?
- Atomic write: Which records must commit or roll back together?
- Downstream delivery: How are timeouts, duplicates, and retries handled?
Primary sources
Read the machinery, not the hype.
- Ethereum accounts — addresses, private keys, externally owned accounts, and contract accounts.
- Ethereum transactions — signatures, gas, broadcast, block inclusion, and finality.
- Ethereum blocks — ordered batches, validators, execution, and state roots.
- Ethereum smart contracts — programs and state at on-chain addresses.
- Ethereum DeFi — swaps, lending, pools, collateral, and the risks around them.
- Fireblocks capabilities — vault accounts, asset wallets, MPC-CMP, policies, and transaction operations.
- Fireblocks create transactions — transfers, contract calls, sources, and destinations.
- Fireblocks transaction monitoring — status updates and webhook guidance.
The Fasset journey is a teaching model based on the service boundaries in the provided local projects. Names, amounts, and UI are simplified; production security, compliance, and operational details are intentionally not reproduced.