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.

StageWhat you doAPIs used
1. ValidateCheck the recipient address, simulate the result, estimate fees, get the nonceisContract · eth_simulateV1 · eth_estimateGas · eth_feeHistory · getNextNonceByAccount
2. SignBuild the transaction from the validated values and sign it with your keyYour own system
3. ExecuteSubmit the signed transaction to the networketh_sendRawTransaction
4. ObserveReceive block inclusion and execution results, then determine finalityWebhook ADDRESS_ACTIVITY · eth_getTransactionReceipt · eth_getBlockByNumber
5. ReconcileMatch the fee and transferred amount against your ledgergetTransactionByHash · 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-KEY header of every request.
  • Endpoint:
    • RPC API: https://ethereum-sepolia.nodit.io/
    • Data API: https://web3.nodit.io/v1/ethereum/sepolia/
  • 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 without 0x. 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 stateTransition condition
VALIDATEDAll stage 1 checks passed
SIGNEDSigning with your key is complete
BROADCASTYou received the transaction hash in the submission response
INCLUDEDThe transaction was included in a block and executed successfully
FINALIZEDThe block containing the transaction became a finalized block
RECONCILEDThe 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 status is 0x1, the call succeeded. If it is 0x0, the call reverted. When the call reverts, returnData contains the failure reason. You can detect failures such as insufficient balance or a blocked contract before signing.
  • In the Transfer event of logs (topics[0] = 0xddf252ad…), compare the sender address, recipient address, and amount (0x3b9aca00 = 1,000 USDC) with the withdrawal request.
  • gasUsed is 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-4
  • gas · maxPriorityFeePerGas · maxFeePerGas: The values you estimated in 1-3
  • chainId: Ethereum Sepolia 11155111 (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.

ErrorMeaningNext action
nonce too lowThe nonce has already been usedRestart from 1-4 and rebuild the transaction with a new nonce
replacement transaction underpricedA pending transaction with the same nonce exists and has a higher feeApply the replacement rules in Recovery
insufficient fundsThe wallet has insufficient ETH to pay gasTop up ETH in the hot wallet and restart from 1-3
already knownThe same transaction has already been submittedDo 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.

  1. 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.
  2. If receipt_status is 1, the execution succeeded. Change the state to INCLUDED and store block_number, receipt_gas_used, and receipt_effective_gas_price.
  3. If receipt_status is 0, 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: to must equal the recipient address in the withdrawal request (case-insensitive).
  • Count: The items in the period and the FINALIZED withdrawals in your ledger must match 1:1 by transactionHash. 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 a cursor. Pass the cursor value 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 fromDate and toDate within 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.