Quickstart
An institutional wallet processes each withdrawal in five stages: pre-validation, signing, execution, tracking, and reconciliation. This Quickstart follows a single transaction on the Ethereum Sepolia testnet, in which a hot wallet withdraws 1,000 USDC to a customer address. At each stage, you call the API that Wallet Node most commonly uses for that step. Your key management system handles signing, and Nodit provides the data before and after the signing step. All examples use Sepolia. On mainnet, only the Endpoint, chain ID, and USDC contract address change, and the flow stays the same.
| Stage | What you do | APIs used |
|---|---|---|
| 1. Validate | Check the recipient address, simulate the result, estimate fees, get the nonce | isContract · eth_simulateV1 · eth_estimateGas · eth_feeHistory · getNextNonceByAccount |
| 2. Sign | Build the transaction from the validated values and sign it with your key | Your own system |
| 3. Execute | Submit the signed transaction to the network | eth_sendRawTransaction |
| 4. Observe | Receive block inclusion and execution results, then determine finality | Webhook ADDRESS_ACTIVITY · eth_getTransactionReceipt · eth_getBlockByNumber |
| 5. Reconcile | Match the fee and transferred amount against your ledger | getTransactionByHash · getTokenTransfersByAccount |
0. Prerequisites
- API Key: Create a project in the Nodit Console and issue an API Key. Include the API Key in the
X-API-KEYheader of every request. - Endpoint:
- RPC API:
https://ethereum-sepolia.nodit.io/ - Data API:
https://web3.nodit.io/v1/ethereum/sepolia/
- RPC API:
- Example values: Replace the placeholders below with your actual values.
{YOUR_API_KEY}: The API Key you issued{HOT_WALLET_ADDRESS}: The institutional hot wallet address that sends the withdrawal{RECIPIENT_ADDRESS}: The customer address that receives the withdrawal{HOT_WALLET_NO_0X}·{RECIPIENT_NO_0X}: The 40-character value of each address without0x. Pad it to 32 bytes inside log topics.- Sepolia USDC contract:
0x1c7d4b196cb0c7b01d743fbc6116a902379c7238. You need Sepolia USDC to run this Quickstart.
If you record the state of each withdrawal request in your internal ledger, every later stage can use that record as its reference. This Quickstart uses the following state sequence.
| Ledger state | Transition condition |
|---|---|
VALIDATED | All stage 1 checks passed |
SIGNED | Signing with your key is complete |
BROADCAST | You received the transaction hash in the submission response |
INCLUDED | The transaction was included in a block and executed successfully |
FINALIZED | The block containing the transaction became a finalized block |
RECONCILED | The fee and transferred amount match your ledger |
1. Validate
Before you submit a transaction, validate that it can be executed. Before you request a signature, check the recipient address, execution result, fee, and nonce.
1-1. Check the Recipient Address
Check whether the receiving address is an externally owned account (EOA) or a contract. If the customer withdrawal address is a contract, it may be unable to handle deposits. In that case, you can hold the withdrawal or require additional approval according to your internal policy.
curl --request POST \
--url https://web3.nodit.io/v1/ethereum/sepolia/blockchain/isContract \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{ "address": "{RECIPIENT_ADDRESS}" }'
{ "result": false }
If result in the response is false, the address is an EOA. If it is true, the address is a contract.
1-2. Simulate the Execution Result
Execute the transaction against the latest block state without actually sending it. When you enable traceTransfers, the response also returns the asset movements produced by the execution as logs. data is the call data for the USDC transfer(to, amount) function.
curl --request POST \
--url https://ethereum-sepolia.nodit.io/ \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_simulateV1",
"params": [
{
"blockStateCalls": [
{
"calls": [
{
"from": "{HOT_WALLET_ADDRESS}",
"to": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"data": "0xa9059cbb000000000000000000000000{RECIPIENT_NO_0X}000000000000000000000000000000000000000000000000000000003b9aca00"
}
]
}
],
"traceTransfers": true
},
"latest"
]
}'
{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
"calls": [
{
"status": "0x1",
"gasUsed": "0xa27f",
"returnData": "0x0000000000000000000000000000000000000000000000000000000000000001",
"logs": [
{
"address": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000{HOT_WALLET_NO_0X}",
"0x000000000000000000000000{RECIPIENT_NO_0X}"
],
"data": "0x000000000000000000000000000000000000000000000000000000003b9aca00"
}
]
}
]
}
]
}
- If
statusis0x1, the call succeeded. If it is0x0, the call reverted. When the call reverts,returnDatacontains the failure reason. You can detect failures such as insufficient balance or a blocked contract before signing. - In the
Transferevent oflogs(topics[0] =0xddf252ad…), compare the sender address, recipient address, and amount (0x3b9aca00= 1,000 USDC) with the withdrawal request. gasUsedis the gas actually consumed by the execution. Compare it with the gas limit you estimate in the next step.
1-3. Estimate the Fee
Estimate the gas limit with eth_estimateGas and the fee rate with eth_feeHistory.
curl --request POST \
--url https://ethereum-sepolia.nodit.io/ \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 2,
"method": "eth_estimateGas",
"params": [
{
"from": "{HOT_WALLET_ADDRESS}",
"to": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"data": "0xa9059cbb000000000000000000000000{RECIPIENT_NO_0X}000000000000000000000000000000000000000000000000000000003b9aca00"
}
]
}'
{ "jsonrpc": "2.0", "id": 2, "result": "0xf60d" }
curl --request POST \
--url https://ethereum-sepolia.nodit.io/ \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 3,
"method": "eth_feeHistory",
"params": [3, "latest", [50]]
}'
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"oldestBlock": "0xb45624",
"reward": [["0xf4240"], ["0x11073e9"], ["0xf4240"]],
"baseFeePerGas": ["0x3e3011ac", "0x3a8a5617", "0x41db0dc4", "0x3da11137"],
"gasUsedRatio": [0.2653948, 0.99984665, 0.24328653333333333],
"baseFeePerBlobGas": ["0x3972155", "0x3c886c4", "0x3897c45", "0x3a4fb02"],
"blobGasUsedRatio": [0.6666666666666666, 0.38095238095238093, 0.38095238095238093]
}
}
The last value of baseFeePerGas is the base fee of the next block. Use this value and the median of reward to estimate the EIP-1559 fee. baseFeePerBlobGas and blobGasUsedRatio apply to blob transactions and are not used in a standard withdrawal.
gas = estimateGas × 1.2 = 62,989 × 1.2 ≈ 75,587 (0x12743)
maxPriorityFeePerGas = median of reward = 0.001 gwei (0xf4240)
maxFeePerGas = next base fee × 2 + priority fee ≈ 1.034 × 2 + 0.001 ≈ 2.069 gwei (0x7b5164ae)
If you set the base fee to 2×, the transaction is not dropped even when the base fee rises over several blocks. The amount you actually pay is the base fee at the time of block inclusion plus the priority fee. maxFeePerGas is only an upper limit.
1-4. Get the Nonce
Retrieve the next nonce of the hot wallet. If a nonce is duplicated, one of the two transactions may be rejected or canceled.
curl --request POST \
--url https://web3.nodit.io/v1/ethereum/sepolia/blockchain/getNextNonceByAccount \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{ "accountAddress": "{HOT_WALLET_ADDRESS}" }'
{ "nonce": "128" }
When all four checks pass, change the ledger state to VALIDATED.
2. Sign
Build an EIP-1559 (type 2) transaction from the values you finalized in stage 1, and sign it in your key management system. Nodit is not involved in this stage and never accesses your keys.
{
"type": "0x2",
"chainId": "0xaa36a7",
"nonce": "0x80",
"to": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"value": "0x0",
"data": "0xa9059cbb000000000000000000000000{RECIPIENT_NO_0X}000000000000000000000000000000000000000000000000000000003b9aca00",
"gas": "0x12743",
"maxPriorityFeePerGas": "0xf4240",
"maxFeePerGas": "0x7b5164ae"
}
nonce:128(0x80), returned in 1-4gas·maxPriorityFeePerGas·maxFeePerGas: The values you estimated in 1-3chainId: Ethereum Sepolia11155111(0xaa36a7)
Store the signed transaction returned by your signing system (the raw value that starts with 0x02f8…) in your ledger, and change the state to SIGNED. If a long time has passed between stage 1 and signing, we recommend that you check the fee and nonce again.
3. Execute
Submit the signed transaction to the blockchain network. {SIGNED_TX} below is the signed raw transaction data you received in stage 2.
curl --request POST \
--url https://ethereum-sepolia.nodit.io/ \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 4,
"method": "eth_sendRawTransaction",
"params": ["{SIGNED_TX}"]
}'
{
"jsonrpc": "2.0",
"id": 4,
"result": "0x5664d487b4a4b690e2df45b1f1d2c3cebf3c4259ef91074504cf4af3f0b0ae1b"
}
result is the transaction hash. Receiving the hash means the node accepted the transaction. It does not mean the transaction is included in a block yet. Store the hash with the withdrawal request and change the state to BROADCAST. From this point, tracking, customer inquiries, and reconciliation all use this hash as the reference.
If the submission request returns an error, handle it according to the error message as follows.
| Error | Meaning | Next action |
|---|---|---|
nonce too low | The nonce has already been used | Restart from 1-4 and rebuild the transaction with a new nonce |
replacement transaction underpriced | A pending transaction with the same nonce exists and has a higher fee | Apply the replacement rules in Recovery |
insufficient funds | The wallet has insufficient ETH to pay gas | Top up ETH in the hot wallet and restart from 1-3 |
already known | The same transaction has already been submitted | Do not resend it. Check the result in stage 4 |
4. Observe
4-1. Receive the Result with Webhook
If you register an ADDRESS_ACTIVITY Webhook on the hot wallet address, Nodit sends a message to your registered URL after the block containing a transaction sent or received by this address is finalized (default setting). If you set isInstant to true when you create the Webhook, Nodit sends the message as soon as it detects the transaction, regardless of block finality. Call volume does not grow as withdrawals increase, which makes Webhook more efficient than polling. You can find the Webhook registration steps in the Webhook Quickstart. To receive the Transaction-type message below, keep withNative at its default value (false) when you create the Webhook. If you set it to true, you receive per-transfer messages such as native transfer and ERC20.
{
"subscriptionId": "4975",
"sequenceNumber": "2",
"protocol": "ethereum",
"network": "sepolia",
"subscriptionType": "WEBHOOK",
"eventType": "ADDRESS_ACTIVITY",
"event": {
"targetAddresses": ["{HOT_WALLET_ADDRESS}"],
"messages": [
{
"type": "transaction",
"hash": "0x5664d487b4a4b690e2df45b1f1d2c3cebf3c4259ef91074504cf4af3f0b0ae1b",
"block_hash": "0xb725be17eef70d404e0ec32016dde2ccf40c7cda4c41c281ba0ad20fd3740b83",
"block_number": 11818537,
"block_timestamp": 1790817744,
"from_address": "{HOT_WALLET_ADDRESS}",
"to_address": "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
"nonce": 128,
"value": "0",
"transaction_type": 2,
"receipt_status": 1,
"receipt_gas_used": 62159,
"receipt_effective_gas_price": 1034965879
}
]
},
"createdAt": "2026-10-01T01:22:27.312Z"
}
When you receive a message, process it in the following order.
- Find the withdrawal request in your ledger by
hash. A hash that is not in the ledger belongs to other activity, such as a deposit, so handle it separately. - If
receipt_statusis1, the execution succeeded. Change the state toINCLUDEDand storeblock_number,receipt_gas_used, andreceipt_effective_gas_price. - If
receipt_statusis0, the transaction was included in a block but the execution failed. No USDC moved, and the gas fee was deducted. Mark the withdrawal as failed and investigate the cause.
The USDC transfer of the same transaction is also delivered as an ERC20-type message (token_address · value · transaction_hash). You can find all message fields in Address Activity.
4-2. Determine Finality
Even a transaction included in a block can disappear if a chain reorganization (reorg) occurs. Ethereum provides the finalized block tag. When the block number containing the transaction is less than or equal to the finalized block number, treat the transaction as final.
curl --request POST \
--url https://ethereum-sepolia.nodit.io/ \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 5,
"method": "eth_getBlockByNumber",
"params": ["finalized", false]
}'
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"number": "0xb4568d",
"hash": "0x…",
"timestamp": "0x6abdba80"
}
}
finalized.number = 0xb4568d = 11,818,637
transaction block_number = 11,818,537
11,818,537 ≤ 11,818,637 → final (FINALIZED)
On Ethereum (the same for Sepolia and mainnet), a block typically takes about 13 minutes (2 epochs) to become finalized. Until the transaction is final, do not mark the withdrawal as complete, and keep it in the INCLUDED state.
5. Reconcile
Close out a finalized transaction by matching its onchain records against your ledger. Verify the fee and transferred amount of a single transaction, and reconcile the full transfer history again on a daily basis.
5-1. Finalize the Fee of a Single Transaction
curl --request POST \
--url https://web3.nodit.io/v1/ethereum/sepolia/blockchain/getTransactionByHash \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{ "transactionHash": "0x5664d487b4a4b690e2df45b1f1d2c3cebf3c4259ef91074504cf4af3f0b0ae1b" }'
{
"transactionHash": "0x5664d487b4a4b690e2df45b1f1d2c3cebf3c4259ef91074504cf4af3f0b0ae1b",
"blockNumber": 11818537,
"from": "{HOT_WALLET_ADDRESS}",
"to": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"value": "0",
"nonce": "128",
"gasUsed": "62159",
"effectiveGasPrice": "1034965879",
"type": "2",
"status": "1"
}
fee (wei) = gasUsed × effectiveGasPrice
= 62,159 × 1,034,965,879
= 64,332,444,072,761 wei ≈ 0.0000643 ETH
Record the calculated fee in your ledger as an ETH deduction from the hot wallet. This value, not the upper limit from the estimation stage (gas × maxFeePerGas), is the amount actually paid.
5-2. Reconcile the Transferred Amount
Retrieve the USDC transfers that left the hot wallet over a period, and reconcile them 1:1 with the withdrawal records in your ledger.
curl --request POST \
--url https://web3.nodit.io/v1/ethereum/sepolia/token/getTokenTransfersByAccount \
--header 'X-API-KEY: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data '{
"accountAddress": "{HOT_WALLET_ADDRESS}",
"relation": "from",
"contractAddresses": ["0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"],
"fromDate": "2026-10-01T00:00:00Z",
"toDate": "2026-10-02T00:00:00Z",
"rpp": 100
}'
{
"rpp": 100,
"range": {
"fromDate": "2026-10-01T00:00:00.000Z",
"toDate": "2026-10-02T00:00:00.000Z"
},
"items": [
{
"from": "{HOT_WALLET_ADDRESS}",
"to": "{RECIPIENT_ADDRESS}",
"value": "1000000000",
"timestamp": 1790817744,
"blockNumber": 11818537,
"transactionHash": "0x5664d487b4a4b690e2df45b1f1d2c3cebf3c4259ef91074504cf4af3f0b0ae1b",
"logIndex": 187,
"contract": {
"address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
"symbol": "USDC",
"decimals": 6
}
}
]
}
Use the following reconciliation criteria.
- Amount:
value÷ 10^decimals= 1,000,000,000 ÷ 10^6 = 1,000 USDC must equal the withdrawal request amount. - Counterparty:
tomust equal the recipient address in the withdrawal request (case-insensitive). - Count: The
itemsin the period and theFINALIZEDwithdrawals in your ledger must match 1:1 bytransactionHash. A transfer that is not in the ledger may be an unauthorized withdrawal, so investigate it immediately. - Pagination: If the results exceed
rpp, the response includes acursor. Pass thecursorvalue in the next request to continue retrieving results. - Address comparison: The Data API returns addresses in checksum format (mixed case). Convert addresses to lowercase before you compare them with the addresses in your ledger.
- Query period: The Data API retains only the last 90 days of data on Sepolia. Specify
fromDateandtoDatewithin this range.
When every item matches, change the state to RECONCILED to close out the withdrawal.
APIs Used
This is the list of APIs used in this Quickstart. You can find all networks and APIs that Wallet Node supports in Networks.
- Data API
Is ContractDetermines whether the recipient address is a contract30 CU - RPC
eth_simulateV1Checks the execution result without sending the transaction35 CU - RPC
eth_estimateGasEstimates the gas required to execute a transaction20 CU - RPC
eth_feeHistoryRetrieves the fee history of recent blocks10 CU - Data API
Get Next Nonce by AccountRetrieves the next nonce of an address30 CU - RPC
eth_sendRawTransactionSubmits a signed transaction and returns the transaction hash40 CU - Webhook
ADDRESS_ACTIVITYDelivers the activity of registered addresses - RPC
eth_getTransactionReceiptRetrieves the receipt of a transaction16 CU - RPC
eth_getBlockByNumberRetrieves block information by block number or tag31 CU - Data API
Get Transaction by HashRetrieves a single transaction by transaction hash80 CU - Data API
Get Token Transfers by AccountRetrieves the token transfers of an address150 CU