T3 · From Code to an On-Chain Transaction
What must code do between construction and confirmation?
- Capabilities practiced
- BuilderOnchain Literacy
- Hands on
- Send a testnet transaction from code with deliberately low gas so it gets stuck, then replace it with the same nonce.
- AI Lab
- Have AI list ten transaction failure scenarios and their error messages. Reproduce three of them on a testnet yourself.
A Real-World Problem
You launch a withdrawal feature. A user enters an amount, clicks Confirm, and the button spins.
In the first week, support forwards three kinds of complaints:
- **"The money was deducted, but the recipient got nothing."**You check a block explorer. The transaction is on-chain with a small red icon; the fee was charged, but the amount did not move.
- **"It has said processing for half an hour."**Your logs show a transaction hash and a successful submission, but the transaction appears in no block and there is no error.
- **"Why did I withdraw twice?"**The impatient user clicked again. Both transactions succeeded.
These problems share a cause: your code knows only that it sent a request, not what happened afterward.
What happens between a transaction leaving your process and a user being able to say "the funds arrived"? How can each step fail?
Thought Experiment
Imagine a transaction as a registered letter with postage paid, sent through a post office that follows rules rigidly.
There are four rules:
**First, every letter needs a number, starting at 0, one per letter, with no gaps.**The post office delivers in numerical order. If letter 5 has not been handled, letter 6 stays in the sorting room. It does not error; it just stops moving.
**Second, you set the postage yourself.**Each round, the sorter chooses the letters with the highest offers. A low-postage letter is not returned. It waits until the office is quiet enough to handle it.
Third, you may send a second letter with the same number.If its postage is sufficiently higher, it replaces the first. This isthe only way to cancel or speed up a submitted letter: you cannot retrieve it, but you can supersede it with another of the same number.
**Fourth, and most crucial: the post office receiving and delivering the letter does not mean the recipient accepted it.**The recipient may reject delivery, yet postage is still charged. In the post office ledger it says "delivered"; for your business the task failed.
Compare these rules with the opening complaints:
- "Money deducted but not received" = rule four: delivery was attempted but rejected.
- "Processing forever" = rule one or two: a number was skipped or postage was too low.
- "Withdrew twice" = your retry used a new number, creating two independent letters that were both delivered.
These are not three unrelated bugs. They are three symptoms of one missing model.
Your Decision
When should the withdrawal interface show "Withdrawal successful"?
What Happens
The choices pause the same pipeline at four different points.
| When you declare success | What you actually know | What you do not yet know |
|---|---|---|
| After signing | The user agreed to the transaction | Whether it was sent |
| After obtaining a hash | A node received it | Whether it will be included, replaced, or dropped |
| After inclusion in a block | It was executed | Whether it succeeded or reverted |
| After a successful receipt and confirmations | It succeeded and is unlikely to be reversed soon | The possibility of an extreme deep reorganization |
The conclusion: this pipeline has five steps, not one. Each has its own failure modes, and they look different. Some throw exceptions, some return error codes, and some return nothing and just stop moving.
The last kind is most dangerous. You handle thrown errors, but silent stalls may go unnoticed until support calls.
Build a Model
A five-step pipeline:
- Construct
- Sign
- Broadcast
- Execute
- Confirm
What each step does and how it can fail:
| Step | What your code does | Typical failure | Symptom |
|---|---|---|---|
| Construct | Set to, value, data, nonce, gas limit, fee cap, and chain ID | Wrong nonce, amount units, or chain ID | Immediate error later, or worse: success on the wrong chain |
| Sign | Use a private key to sign transaction content, yielding broadcastable bytes | Wrong key or format for another chain | Rejected on broadcast or recovers a different signer |
| Broadcast | Hand those bytes to a node | Low fee, occupied or skipped nonce, insufficient base-fee cap | Some error immediately; others are silently accepted but never move |
| Execute | A node includes and executes it, producing a receipt | Contract revert, gas exhaustion, changed state invalidates a condition | In a block, receipt says failure, fee still charged |
| Confirm | Wait for later blocks | Reorganization, same-nonce replacement, dropped from a cleaned mempool | A transaction once found may disappear later |
Four details deserve separate attention.
1. There are two ways to read the next nonce; using the wrong one can stall you.
When asking a node for the address's next nonce, you can count only on-chain transactions or include pending mempool transactions. Use the latter when sending several in a row; otherwise the second may reuse the first's nonce and be rejected. But there is a trap: if the first never enters a block, every later transaction waits behind it.
2. An inadequate fee may not throw an error; it may simply stop moving.
This is counterintuitive. A traditional request succeeds or fails. Here there is a third state: accepted but pending indefinitely. Your code has a hash and appears healthy, while nothing happens.
3. A failed transaction still pays for the work already performed.
From the chain's perspective, nodes really did spend resources executing it. To the user, it looks like "I paid and got nothing." Your error message must say so clearly, or users may think you took their money.
4. The only way to cancel a transaction is to send another with the same nonce.
There is no "withdraw" command. You can send a new zero-value transaction to yourself, with the same nonce and a higher fee. Whichever enters a block first invalidates the other. The replacement has a different hash, so track both or your system may show failure despite an on-chain success.
What It Is Called
The strictly increasing sequence number of transactions from one address, starting at 0 with no gaps.
It prevents a transaction from being replayed and sets the execution order for that address.
In cryptography, nonce means "number used once," as in T2. Here it means a counter. Same name, different concept.
The gas limit caps the computation a transaction may consume; the fee cap limits what you are willing to pay per unit.
They fail differently: **too little gas means the transaction may run out mid-execution and still pay fees; too low a fee cap means it will not execute and may remain pending.**Confusing them sends debugging in the wrong direction.
The signed bytes ready to broadcast.
Their hash is fixed before broadcast. "Having a hash" does not mean "the chain knows this transaction." This distinction saves substantial debugging time.
A node's local collection of transactions not yet included in blocks.
The key word is local. It is not part of the chain. Mempools differ across nodes; nothing guarantees your transaction exists in all of them or remains there. T4 explains the consequences.
A new transaction with the same nonce and higher fee that displaces the old one in the mempool.
Speeding up and canceling are two uses of the same mechanism. Nodes require a minimum fee bump and reject smaller increases, often with an error saying the replacement is underpriced.
Confirmations count the blocks built on top of a transaction's block. Finality means the protocol has marked the transaction as no longer reversible.
**They differ.**Confirmations express a probability judgment; finality is a protocol commitment. Explicit block tags can distinguish "latest" from "finalized" in queries, as T4 discusses.
Hands-on
**Use a testnet throughout. Its coins have no real value and no real assets are involved.**Do not use a wallet holding real funds; make a dedicated test wallet.
First, compare the two ways to read a nonce.
curl -s -X POST <your-testnet-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount",
"params":["<your-address>","latest"]}'
curl -s -X POST <your-testnet-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionCount",
"params":["<your-address>","pending"]}'The two values should currently match. Check again after the later steps and you will see them diverge.
Check the current fee level.
curl -s -X POST <your-testnet-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_gasPrice","params":[]}'The response is wei in hexadecimal. Convert it to decimal, then divide by 10 to the ninth power for gwei. Write that number down; next you will use a value far below it.
Send a normal transaction to verify the path works.
Use your wallet to send a tiny amount to another test address you own. Save the hash, then query:
curl -s -X POST <your-testnet-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionByHash","params":["<hash>"]}'Observe blockNumber.**If it is null, the transaction is still pending.**This distinguishes "broadcast" from "included."
Deliberately stall a transaction.
Open advanced wallet settings; the name varies, so look for "custom nonce" or "advanced gas controls." Send a transfer, manually set the fee cap to one tenth of the value from step 2, and keep the default nonce.
Send it. The wallet supplies a hash and shows no error, but the eth_getTransactionByHash result's blockNumber stays null.
**This is the hardest kind of failure to investigate: everything looks normal, but nothing moves.**Search for the hash in a block explorer; it may show "pending" or nothing at all.
Send another transaction and see how it is blocked.
Leave the previous one pending and send another ordinary transfer with the default fee. It gets the next nonce.
Despite its sufficient fee, it also remains pending.**The earlier nonce must be handled before any later one.**Query both nonce variants from step 1; they now differ by two.
One stuck transaction can block an address's entire withdrawal queue—one of the most common incidents in payment systems.
Replace the stalled transaction with the same nonce.
Find the transaction from step 4 in your wallet and choose "Speed Up" or "Cancel." The wallet constructs another transaction with the same nonce and a higher fee.
To do this manually, set the new transaction's nonce to that of the stalled one and its fee cap above twice the current level. An insufficient bump is rejected with an underpriced-replacement error.
After replacement:
- Query the old hash with
eth_getTransactionByHash; it returns null—that transaction no longer exists in the node's view. - Query the new hash;
blockNumberis no longer null. - The blocked second transaction can now enter a block as well.
Finally, verify whether a transaction succeeded.
curl -s -X POST <your-testnet-RPC-URL> \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getTransactionReceipt","params":["<new-hash>"]}'Check two fields: status is 0x1 for success and 0x0 for failure; gasUsed is the amount actually charged.
**Put this step in your code.**Declaring success without checking receipt status invites the first complaint from this chapter.
AI Lab
Ask in three steps.
Step one:
List ten ways an Ethereum transaction can fail between construction and confirmation.
For each, provide the stage, a typical literal node error message, what the user sees,
whether a fee is charged, and the correct response. Use a table.
Step two:
Which of these ten throw no error and merely appear to remain stuck?
For each, explain how my backend should detect it and how often it should check.
Step three:
Give exact steps to reproduce failures 2, 5, and 8 on a testnet,
using only JSON-RPC and an ordinary wallet, with no SDK.Step two is the point of the Lab. You already know to handle thrown errors; silent failures cause production incidents.
Step three tests the model's claims. If you reproduce them, some error strings may differ. Node clients use different wording, and a model may blend messages from several implementations.
**Run the experiment yourself: your error mapping must be based on the actual node you use, not a model's memory.**T14 turns that map into user-facing messages.
Verify these points yourself after using AI
- You can reproduce each quoted error string on a real node instead of relying on a remembered approximation
- It distinguishes a low transaction fee from an underpriced replacement transaction
- It does not wrongly claim failed transactions pay no fee
- It does not confuse the consequences of a low gas limit with a low fee cap
- Its SDK methods and parameter order match official documentation
- Its example converts wei, gwei, and hexadecimal correctly
- It does not treat inclusion in a block as proof of success
Real-World Case
A service submits a withdrawal with too low a fee cap during a fee surge. It throws no error; it simply does not move.
All later withdrawals get higher nonces and queue behind it. Thousands accumulate over hours, support is overwhelmed, and monitoring raises no alert—because not one request has failed.
The immediate repair is one action: resend with the same nonce and a higher fee. Recognizing the need is the hard part.
The lasting engineering changes are a timeout that reprices and resends transactions not yet on-chain, plus an alert on the gap between pending and latest nonce values.
Someone submitted a transaction whose fee exceeded the transfer amount by three orders of magnitude, a large sum at the time.
A suspected cause was swapping two fields when constructing the transaction: a fee parameter was placed where the amount belonged, and vice versa.
The lesson is more concrete than "be careful": transaction-construction code needs unit assertions and maximum-value checks. Humans will enter wrong values; code must catch them before broadcast.
A user clicks "Speed Up" in the wallet for a stalled transaction. The wallet resends with the same nonce and a higher fee, and the replacement succeeds on-chain.
But the business system stored only the old hash. It keeps polling that hash, finds nothing, eventually marks the withdrawal failed, and releases the balance—even though the money has already left on-chain.
Track the final outcome for an address and nonce, not one hash alone. T14 treats replacement as a distinct state in the state machine.
A contract call reverts during execution. Its receipt says failure, and a fee is charged.
Some wallets and frontends still show a green success message because they check only that the transaction entered a block. The user sees success while the recipient sees nothing.
This bug is so common it should be a rule of thumb: check the status field explicitly; never assume success.
Change One Variable
Both processes read the same nonce and construct different transactions using that number. The first to arrive is accepted; the second is treated as a replacement—either rejected for an insufficient fee bump or allowed to displace the first.
The result is intermittent lost orders that are hard to reproduce.
A lock alone is insufficient. Use a single nonce allocator, with allocation and persistence of "transaction submitted" in the same database transaction. T15 develops that design.
The signed contents are fixed; the fee cap cannot be changed. The transaction waits for fees to fall, perhaps minutes or days.
Production systems cannot simply sign and forget.**A background job must watch pending transactions and resend with the same nonce and a higher fee after a timeout.**Its own loop must be idempotent or it may emit many mutually replacing transactions.
That creates two independent transactions. Both can execute, so money leaves twice.
This is the third opening complaint. Prevent it with business-level idempotency: one withdrawal request maps to one nonce and one transaction body. A retry rebroadcasts the same signed bytes rather than constructing another transaction.
Instead of an increasing nonce, a transaction carries a recent blockhash. It expires: an expired transaction does not remain pending; it becomes invalid, requiring reconstruction and a new signature.
The engineering problem changes from "stuck" to "expired, but did it actually get through?" Solana transactions also have failure states, and receipt errors still matter. T6 covers the account model; T14 compares the state machines.
Takeaway Questions
The address initiating a transaction always pays its fee, even if the transaction fails. This creates a product choice: should users pay their own fees, or will you sponsor them? Sponsorship requires a hot wallet, nonce management, and abuse controls—all costs described in this chapter.
When a transaction stalls, the user waits and you bear support costs. When execution fails, the user pays the fee. When the system cannot reconcile, you often bear the loss. Three failure modes allocate responsibility differently, so code must distinguish them instead of returning one generic "transaction failed" error.
An AI Agent sending transactions may retry a timeout by constructing and signing a new transaction instead of resending the original, causing two payouts. The model does not reliably understand nonce-based idempotency. Encode retry logic in your code rather than letting it decide on the fly.
Chapter Check
No. The hash is calculated from transaction content and is fixed before broadcast. It names the transaction; it is not a receipt from the chain.
Use eth_getTransactionByHash and inspect its blockNumber to determine inclusion; use the receipt's status to determine success.
No. This distinction is central.
Insufficient gas limit: the transaction may enter a block and run out of gas during execution; it reverts but still pays a fee. Insufficient fee cap: it is not included and stays pending in the mempool, paying nothing while making no progress.
One is "paid but failed," the other "did not pay and did not execute." They need different debugging and user messages.
There is no direct cancellation command. Send another transaction with the same nonce and a sufficiently higher fee to replace it.
A common choice is a zero-value transfer to yourself. The new hash differs from the old one, so track both or track the outcome by address and nonce.
Transactions from one address must execute in strict nonce order, with no gaps.
If nonce 5 is unresolved, transactions 6, 7, and 8 cannot execute however high their fees are. They are not merely low priority; the protocol has not reached their turn.
A single stalled transaction can stop the address's withdrawal queue. High-throughput payment systems may spread transactions across several sending addresses to limit this risk.
There is no one standard answer. Check these points:
- At minimum, represent awaiting signature, signed, broadcast, included, confirmed, execution failed, replaced, and resent after timeout. Merging "broadcast" with "successful" is inadequate.
- Allocate nonces from one source and commit allocation and the business record together.
- On retry, do you rebroadcast the same signed bytes or reconstruct the transaction? Only the former is idempotent.
- Does a background job handle timeouts before inclusion without issuing a storm of mutually replacing transactions?
- Do you monitor the gap between pending and latest nonce values?
- Do you explicitly check the receipt's status before declaring success?
- If the user speeds up a transaction in the wallet and changes its hash, will your system mistakenly mark it failed?
If you included points 3, 4, and 7, you are building more than a demo.
Key idea
Construction, signing, broadcast, execution, and confirmation each have their own failure modes.