Crypto OS
Technical Crypto OSStage 1 · Blockchain Developer Mental Model

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 successWhat you actually knowWhat you do not yet know
After signingThe user agreed to the transactionWhether it was sent
After obtaining a hashA node received itWhether it will be included, replaced, or dropped
After inclusion in a blockIt was executedWhether it succeeded or reverted
After a successful receipt and confirmationsIt succeeded and is unlikely to be reversed soonThe 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:

  1. Construct
  2. Sign
  3. Broadcast
  4. Execute
  5. Confirm
The first three steps occur in your process, the fourth in nodes, and the fifth over time. Failures in these places require different handling.

What each step does and how it can fail:

StepWhat your code doesTypical failureSymptom
ConstructSet to, value, data, nonce, gas limit, fee cap, and chain IDWrong nonce, amount units, or chain IDImmediate error later, or worse: success on the wrong chain
SignUse a private key to sign transaction content, yielding broadcastable bytesWrong key or format for another chainRejected on broadcast or recovers a different signer
BroadcastHand those bytes to a nodeLow fee, occupied or skipped nonce, insufficient base-fee capSome error immediately; others are silently accepted but never move
ExecuteA node includes and executes it, producing a receiptContract revert, gas exhaustion, changed state invalidates a conditionIn a block, receipt says failure, fee still charged
ConfirmWait for later blocksReorganization, same-nonce replacement, dropped from a cleaned mempoolA 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

NonceNonce / sequence number

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.

Gas Limit / Fee CapGas limit / fee cap

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.

Raw TransactionRaw transaction

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.

MempoolMempool

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.

ReplacementReplacement transaction

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.

Confirmation / FinalityConfirmations / finality

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

Hands onSend a testnet transaction, deliberately stall it, then replace it with the same nonceAny testnet + curl + a wallet that allows manual nonce entryFree test coins from a faucet

**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; blockNumber is 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

AI LabAsk AI for ten transaction failure modes and errors, then reproduce three on a testnetLevel 2 · AI Copilot

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

One stuck nonce blocks the entire withdrawal queue

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.

The fee exceeds the transfer amount by a thousandfold2019

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 speeds up a transaction and the system loses track

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.

The transaction fails, but both wallet and frontend show success

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

What ifTwo service processes send transactions from the same address at once

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.

What ifThe network base fee rises tenfold after you sign

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.

What ifA user retry gets a new nonce

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.

What ifSwitch to Solana

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

5
Who is paying?

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.

9
Who bears the risk?

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.

17
Who bears the loss when AI gets it wrong?

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

Key idea

Construction, signing, broadcast, execution, and confirmation each have their own failure modes.

Finished this chapter’s lab? Mark it completeView all progress

On this page