Address transfer monitoring — User guide
Version: 2.0.0 | Audience: integrator developers
Table of contents
- 1. Product overview
- 2. Quick start
- 3. Data ingestion module
- 4. Transaction query module
- 5. Address management module
- 6. WebSocket real-time push
1. Product overview
Introduction
Address transfer monitoring is designed for business systems that need to track on-chain transfer activity for wallet addresses. The service continuously receives and parses multi-chain transaction data, persists normalized transfer records, and exposes the results through HTTP queries and WebSocket pushes.
The goal is to let integrators monitor selected addresses without operating their own nodes or building a full transaction parsing pipeline. Integrators only need to maintain their monitored address list and keep their delivery connection available.
Core capabilities
| Capability | Description |
|---|---|
| Multi-chain transaction monitoring | Unified ingestion for EVM-compatible chains, Solana, Tron, Bitcoin, and other supported networks. |
| Real-time push | Pushes on-chain transactions to subscribed clients through WebSocket. |
| Historical query | Queries historical transactions for addresses through HTTP APIs. |
| Address subscription management | Adds and removes monitored addresses dynamically, with business isolation based on the public API key. |
| Webhook notification | Delivers transaction data to an HTTP callback configured by the service side. |
Supported chains
EVM-compatible chains
| Chain | chain_type | Mainnet chain_id |
|---|---|---|
| Ethereum | ethereum | 1 |
| BNB Smart Chain | bsc | 56 |
| Polygon | polygon | 137 |
| Arbitrum One | arbitrum | 42161 |
| Optimism | optimism | 10 |
| Base | base | 8453 |
Non-EVM chains
| Chain | chain_type | Mainnet chain_id |
|---|---|---|
| Solana | solana | 1 |
| Tron | tron | 1 |
| Bitcoin | bitcoin | 1 |
chain_typeis case-insensitive in requests. The service normalizes it to lowercase.
End-to-end data flow
flowchart TD
UPSTREAM[On-chain data push<br/>Stream / Webhook]
API[Integrator system]
CL_STREAM[POST /stream<br/>POST /webhook]
PROCESSOR[Chain processors<br/>EVM / Solana / Tron / Bitcoin]
DB[(Database)]
WS_DO[SubscriptionManager<br/>Durable Object]
WS_CLIENT[WebSocket client]
WEBHOOK_OUT[Outbound Webhook callback]
CRON[Cron job<br/>Every minute]
UPSTREAM --> CL_STREAM
CL_STREAM --> PROCESSOR
PROCESSOR --> DB
PROCESSOR --> WS_DO
WS_DO -->|Real-time push| WS_CLIENT
WS_DO -->|Webhook notification| WEBHOOK_OUT
API -->|POST /api/v1/transfer| DB
API -->|GET /transaction/transaction/tx| DB
CRON -->|Compensate pending transactions| DB
Data flow notes:
- Configured upstream data pushes deliver raw payloads to
/streamor/webhook. The service provider deploys and authorizes upstream ingestion, so ordinary integrators do not need to connect to upstream infrastructure directly. - The service dispatches each payload to the corresponding chain processor by chain type.
- Parsed transaction data is written to the database and triggers subscription notifications.
- Transactions that match monitored addresses are pushed to connected WebSocket clients in real time.
- Integrators can query historical transactions through HTTP APIs.
- A cron job runs once per minute to compensate unfinished pending transactions.
Core concepts
Chain identity: chain_type and chain_id
Each chain is identified by two fields:
chain_type: the chain type name, such asethereum,solana, ortron.chain_id: the chain ID used to distinguish mainnets and testnets, such as1for Ethereum Mainnet and11155111for Sepolia.
These two fields are used by every API that involves chain-specific data.
Public authentication: X-API-Key
The public APIs currently exposed by the service use the X-API-Key request header for API authentication and billing identification. Address management, transaction query, and WebSocket connections should all carry this header.
X-API-Key: your-api-key
Standard response format
Most APIs follow this JSON response structure:
{
"code": 0,
"message": "ok",
"data": {}
}
| Field | Type | Description |
|---|---|---|
code | integer | Business status code. 0 means success. |
message | string | Response message. Successful responses usually use ok. |
data | any | Business data. Failed responses usually return null. |
Some legacy APIs use
msginstead ofmessage. Follow the specific API documentation for each endpoint.
Error response format
Failed requests return a structure similar to:
{
"code": 400,
"msg": "Invalid request",
"data": null
}
Common HTTP status codes:
| Status | Meaning |
|---|---|
400 | Request parameter validation failed. |
401 | Authentication failed for the stream secret. |
403 | No permission because the API key is invalid. |
504 | Request timed out after 15 seconds. |
500 | Internal service error. |
Authentication
All public APIs use the X-API-Key request header. The key is used for API authentication, caller identification, and billing statistics.
Public API key (X-API-Key)
All public API requests should carry:
X-API-Key: your-api-key
Internal stream secret (Content-Secret-X)
The /stream ingestion entry is used by the service provider or forwarding layer, and is not a public entry for ordinary integrators. The related secret is configured internally by the service provider.
Supported network identifiers (x-chain-net)
When calling the on-chain JSON-RPC proxy API (/api/v1/quicknode), specify the target network through the x-chain-net request header:
| Network | x-chain-net value |
|---|---|
| Ethereum Mainnet | ethereum-mainnet |
| Ethereum Sepolia | ethereum-sepolia |
| BNB Mainnet | bsc-mainnet |
| BNB Testnet | bsc-testnet |
| Polygon Mainnet | polygon-mainnet |
| Polygon Amoy | polygon-amoy |
| Solana Mainnet | solana-mainnet |
| Solana Devnet | solana-devnet |
| Tron Mainnet | tron-mainnet |
| Bitcoin Mainnet | bitcoin-mainnet |
| Arbitrum Mainnet | arbitrum-mainnet |
| Optimism Mainnet | optimism-mainnet |
| Base Mainnet | base-mainnet |
2. Quick start
This section helps you complete a basic integration in about 10 minutes, including real-time notifications and historical queries for monitored addresses.
Prerequisites
Before starting, obtain the following credential from the service provider:
| Credential | Purpose | Example |
|---|---|---|
| API key | API authentication, caller identification, and billing statistics. | sk-xxx |
Store the service base URL. All examples below use BASE_URL.
BASE_URL=https://api.gelabs.org
API_KEY=your-api-key
Typical integration flow
flowchart LR
A[Add monitored addresses] --> B[Establish WebSocket connection]
B --> C[Receive real-time transaction pushes]
A --> D[Query historical transactions]
Step 1: Add monitored addresses
Add wallet addresses that you want to monitor to your application's address list. The example below adds one Ethereum address:
curl -X POST "$BASE_URL/api/v1/address/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"wallets": [
{
"chain_type": "ethereum",
"address": ["0x1234567890abcdef1234567890abcdef12345678"]
}
]
}'
Expected response:
{
"code": 0,
"message": "ok",
"data": "success"
}
You can add addresses for multiple chains in one request:
curl -X POST "$BASE_URL/api/v1/address/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"wallets": [
{
"chain_type": "ethereum",
"address": ["0xabc...001", "0xabc...002"]
},
{
"chain_type": "solana",
"address": ["So1ana...address"]
}
]
}'
Step 2: Establish a WebSocket connection and receive real-time pushes
After the address is added successfully, establish a WebSocket connection to receive real-time transactions.
Quick test with wscat
# Install wscat if it is not installed yet.
npm install -g wscat
# Establish the connection.
wscat -c "$BASE_URL/transaction/transaction/ws" \
-H "X-API-Key: $API_KEY"
After the connection succeeds, the server immediately sends a confirmation message:
{
"id": "msg-xxx",
"time": 1745000000000,
"type": "connected",
"code": 0,
"data": {
"clientId": "client-abc123",
"appId": "current-business-identity"
},
"msg": "success"
}
Receive real-time transactions
When a monitored address has an on-chain transaction, the server pushes:
{
"id": "msg-tx-001",
"time": 1745000000000,
"type": "transaction",
"code": 0,
"data": {
"chain_type": "ethereum",
"chain_id": 1,
"height": 21000000,
"txhash": "0xabc123...",
"sender": "0x1234...",
"receiver": "0x5678...",
"amount": "1000000000000000000",
"symbol": "ETH",
"decimals": 18,
"txStatus": "success",
"listenAddress": ["0x1234..."],
"is_reorg_resend": false
},
"msg": "success"
}
After receiving a transaction message, send an ACK to avoid server retries:
{
"id": "ack-001",
"type": "transactionACK",
"data": {
"chain_type": "ethereum",
"chain_id": 1,
"txHash": "0xabc123..."
}
}
Step 3: Query historical transactions
In addition to real-time pushes, you can query historical transactions for an address through HTTP:
curl -X POST "$BASE_URL/api/v1/transfer" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"chain_type": "ethereum",
"chain_id": 1,
"address": "0x1234567890abcdef1234567890abcdef12345678",
"limit": 10
}'
Expected response, shortened:
{
"code": 1,
"msg": "success",
"data": [
{
"chain_type": "ethereum",
"chain_id": 1,
"tx_hash": "0xabc123...",
"sender": "0x1234...",
"receiver": "0x5678...",
"amount": "1000000000000000000",
"symbol": "ETH",
"tx_status": "success"
}
]
}
Complete integration example (Node.js)
const WebSocket = require('ws');
const BASE_URL = 'https://api.gelabs.org';
const API_KEY = 'your-api-key';
const ADDRESS = '0x1234567890abcdef1234567890abcdef12345678';
async function addAddress() {
const res = await fetch(`${BASE_URL}/api/v1/address/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({
wallets: [{ chain_type: 'ethereum', address: [ADDRESS] }],
}),
});
const data = await res.json();
console.log('Add address result:', data);
}
function connectWebSocket() {
const ws = new WebSocket(
`${BASE_URL.replace('https', 'wss')}/transaction/transaction/ws`,
{ headers: { 'X-API-Key': API_KEY } }
);
ws.on('open', () => {
console.log('WebSocket connection established');
setInterval(() => {
ws.send(JSON.stringify({ id: Date.now().toString(), type: 'ping', data: {} }));
}, 30000);
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
switch (msg.type) {
case 'connected':
console.log('Connection confirmed, clientId:', msg.data.clientId);
break;
case 'transaction':
console.log('Transaction received:', msg.data.txhash);
ws.send(JSON.stringify({
id: `ack-${msg.id}`,
type: 'transactionACK',
data: { chain_type: msg.data.chain_type, chain_id: msg.data.chain_id, txHash: msg.data.txhash },
}));
break;
case 'pong':
break;
}
});
ws.on('close', () => {
console.log('Connection closed, reconnecting in 5 seconds...');
setTimeout(connectWebSocket, 5000);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
});
}
async function main() {
await addAddress();
connectWebSocket();
}
main();
3. Data ingestion module
The data ingestion module receives authorized raw on-chain data pushed to this service through Stream or Webhook entries, then dispatches it to the corresponding chain processor for parsing and persistence. The platform maintains the push-side network and providers. Integrators only need to use the public APIs documented here.
API list
| Method | Path | Description | Authentication |
|---|---|---|---|
| POST | /stream | Signed stream ingestion entry, recommended for production | Yes, stream secret |
| POST | /webhook | Compatibility Webhook entry | No |
| POST | /api/v1/quicknode | On-chain JSON-RPC proxy | No, target chain network header required |
Data processing flow
flowchart LR
IN[Stream request] --> PARSE[Parse batch metadata<br/>Chain type / Network]
PARSE --> PROC[Chain processor\nEVM / Solana / Tron / Bitcoin]
PROC --> DB[(Database write)]
PROC --> NOTIFY[Trigger subscription notification]
NOTIFY --> WS[WebSocket push]
NOTIFY --> WH[Webhook callback]
Processing is asynchronous. After receiving a stream request, the service returns a response immediately. Actual parsing and database writes run in the background.
Stream ingestion
API path: POST /stream
Purpose
Receives authorized raw on-chain data payloads. This is one of the main production entries for on-chain data.
Authentication
Authentication required: the request must carry the stream secret header, otherwise
401is returned.
Content-Secret-X: your-stream-secret
Chain network identification
The service identifies the source chain network through the following request headers, in priority order:
cl-stream-networkstream-network
Example
curl -X POST "$BASE_URL/stream" \
-H "Content-Secret-X: your-stream-secret" \
-H "cl-stream-network: ethereum-mainnet" \
-H "Content-Type: application/octet-stream" \
--data-binary @payload.bin
Response
{
"code": 0,
"message": "ok",
"data": null
}
Webhook ingestion
API path: POST /webhook
Purpose
A legacy-compatible Webhook entry with the same processing logic as /stream. Main differences from /stream:
- No stream secret validation: the current version does not validate
Content-Secret-Xby default. - Use cases: legacy-compatible Webhooks or internal test scenarios.
Production environments should prefer
/streamwith secret validation to avoid unauthorized data injection.
Example
curl -X POST "$BASE_URL/webhook" \
-H "cl-stream-network: ethereum-mainnet" \
-H "Content-Type: application/octet-stream" \
--data-binary @payload.bin
Response
{
"code": 0,
"message": "ok",
"data": null
}
On-chain JSON-RPC proxy
API path: POST /api/v1/quicknode
Purpose
Proxies JSON-RPC requests to the corresponding on-chain RPC endpoint based on the selected network. It can be used to query blocks, balances, and other standard methods. The quicknode segment in the path is a historical compatibility name and is not tied to a specific provider.
Required headers
Target network required: the
x-chain-netrequest header must be present. Otherwise, the service returns a500error.
x-chain-net: ethereum-mainnet
Request body fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
method | string | No | eth_blockNumber | JSON-RPC method name. |
params | array | No | [] | JSON-RPC parameter array. |
Example
# Query the latest block number.
curl -X POST "$BASE_URL/api/v1/quicknode" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "x-chain-net: ethereum-mainnet" \
-d '{"method": "eth_blockNumber", "params": []}'
# Query an account balance.
curl -X POST "$BASE_URL/api/v1/quicknode" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "x-chain-net: ethereum-mainnet" \
-d '{"method": "eth_getBalance", "params": ["0x1234...", "latest"]}'
Response
{
"code": 1,
"msg": "success",
"data": "0x14a5b8c"
}
This API returns
code: 1on success, which differs from APIs that use0. Handle it explicitly during integration.
Supported network list
| Network | x-chain-net / cl-stream-network value |
|---|---|
| Ethereum Mainnet | ethereum-mainnet |
| Ethereum Sepolia Testnet | ethereum-sepolia |
| BNB Smart Chain Mainnet | bsc-mainnet |
| BNB Smart Chain Testnet | bsc-testnet |
| Polygon Mainnet | polygon-mainnet |
| Polygon Amoy Testnet | polygon-amoy |
| Solana Mainnet | solana-mainnet |
| Solana Devnet | solana-devnet |
| Tron Mainnet | tron-mainnet |
| Bitcoin Mainnet | bitcoin-mainnet |
| Arbitrum One Mainnet | arbitrum-mainnet |
| Optimism Mainnet | optimism-mainnet |
| Base Mainnet | base-mainnet |
4. Transaction query module
The transaction query module provides multi-dimensional queries for on-chain transaction data, including historical lists by address, batch transaction detail queries, and cross-chain aggregate queries.
API list
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/transfer | Query transaction list by address with cursor pagination |
| POST | /api/v1/transfer/multiple | Multi-chain and multi-address aggregate query |
| POST | /api/v1/transfer/details | Batch query details by transaction hash |
| GET | /transaction/transaction/tx | Query single transaction detail, V2 |
Address transaction query
API path: POST /api/v1/transfer
Purpose
Queries the historical transaction list for one address on a specified chain. It supports filtering by transaction type and token, and uses cursor-based pagination.
Request body fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
address | string | Yes | - | Wallet address to query. |
chain_type | string | No | - | Chain type. If omitted, the service performs a cross-chain query. |
chain_id | integer | No | - | Chain ID. |
tx_type | string | No | "" | Transaction type filter. Empty string means no filtering. |
token | string | No | - | Token contract address used to filter a specific token transfer. |
limit | integer | No | 20 | Maximum number of returned records. Range: 1~100. |
id | integer | No | - | Cursor ID for pagination continuation. |
Example
# Basic query.
curl -X POST "$BASE_URL/api/v1/transfer" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"chain_type": "ethereum",
"chain_id": 1,
"address": "0x1234567890abcdef1234567890abcdef12345678",
"limit": 20
}'
# Filter by token to query USDT transfers.
curl -X POST "$BASE_URL/api/v1/transfer" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"chain_type": "ethereum",
"chain_id": 1,
"address": "0x1234...",
"token": "0xdAC17F958D2ee523a2206206994597C13D831ec7"
}'
Response
{
"code": 1,
"msg": "success",
"data": [
{
"chain_type": "ethereum",
"chain_id": 1,
"height": 21000000,
"tx_time": 1745000000,
"tx_hash": "0xabc123...",
"sender": "0x1234...",
"receiver": "0x5678...",
"amount": "1000000000000000000",
"symbol": "ETH",
"decimals": 18,
"tx_status": "success",
"transfer_type": "native",
"token_transfers": [],
"from_details": [],
"to_details": [],
"internal_transactions": []
}
]
}
This API returns
code: 1on success and usesmsg, which differs from some other APIs.
Multi-address aggregate query
API path: POST /api/v1/transfer/multiple
Purpose
Queries transaction records for multiple chains and multiple addresses and returns the aggregated result.
Request body fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
addresses | array | Yes | - | Multi-chain address groups. At least 1 group and at most 10 groups. |
addresses[].chain_type | string | Yes | - | Chain type. |
addresses[].chain_id | integer | Yes | - | Chain ID. |
addresses[].address | string[] | Yes | - | Address array for this chain. At most 20 addresses per group. |
limit | integer | No | 20 | Maximum returned records. Range: 1~100. |
id | integer | No | - | Cursor ID for pagination continuation. |
Example
curl -X POST "$BASE_URL/api/v1/transfer/multiple" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"addresses": [
{"chain_type": "ethereum", "chain_id": 1, "address": ["0xabc...001", "0xabc...002"]},
{"chain_type": "bsc", "chain_id": 56, "address": ["0xdef...003"]},
{"chain_type": "solana", "chain_id": 1, "address": ["SolanaAddress..."]}
],
"limit": 20
}'
Response
The response format is the same as /api/v1/transfer. data is the aggregated transaction record array.
Batch transaction detail query
API path: POST /api/v1/transfer/details
Purpose
Batch queries transaction details by transaction hash.
Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
chain_type | string | Yes | Chain type. |
chain_id | integer | Yes | Chain ID. |
tx_hash | string or string[] | Yes | A single transaction hash or an array of hashes. |
Example
# Query one transaction.
curl -X POST "$BASE_URL/api/v1/transfer/details" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{"chain_type": "ethereum", "chain_id": 1, "tx_hash": "0xabc123..."}'
# Batch query multiple transactions.
curl -X POST "$BASE_URL/api/v1/transfer/details" \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{"chain_type": "ethereum", "chain_id": 1, "tx_hash": ["0xabc123...", "0xdef456..."]}'
Single transaction detail query
API path: GET /transaction/transaction/tx
Purpose
Queries the full details of one transaction, V2. Main differences from /api/v1/transfer/details:
- Uses GET with query string parameters.
- Supports
no_cacheto force an origin fetch. If there is no local database record, the service can fetch by chain and write the result into cache. - Writes the data into the database cache after a successful origin fetch.
- Returns a mixed field naming structure for historical compatibility. Some fields are camelCase.
Request parameters
| Name | Location | Type | Required | Description |
|---|---|---|---|---|
chain_type | query | string | Yes | Chain type. |
chain_id | query | integer | Yes | Chain ID. |
tx_hash | query | string | Yes | Transaction hash, with or without the 0x prefix. |
no_cache | query | string | No | Pass true or 1 to skip local cache and fetch from origin. |
Example
# Normal query.
curl "$BASE_URL/transaction/transaction/tx?chain_type=ethereum&chain_id=1&tx_hash=0xabc123..."
# Force origin fetch.
curl "$BASE_URL/transaction/transaction/tx?chain_type=ethereum&chain_id=1&tx_hash=0xabc123...&no_cache=true"
Response when the transaction is found
{
"code": 0,
"msg": "success",
"data": {
"chain_type": "ethereum",
"chain_id": 1,
"height": 21000000,
"txTime": 1745000000000,
"txhash": "0xabc123...",
"sender": "0x1234...",
"fee_payer": "0x1234...",
"receiver": "0x5678...",
"transfer_type": "native",
"gasLimit": "21000",
"gasUsed": "21000",
"gasPrice": "20000000000",
"max_fee_per_gas": "30000000000",
"max_priority_fee_per_gas": "1000000000",
"txFee": "420000000000000",
"nonce": "42",
"symbol": "ETH",
"decimals": 18,
"amount": "1000000000000000000",
"txStatus": "success",
"methodId": "",
"methodCall": "",
"l1OriginHash": "",
"fromDetails": [],
"toDetails": [],
"internalTransactionDetails": [],
"tokenTransferDetails": [],
"listenAddress": null,
"is_reorg_resend": false
}
}
Response when the transaction does not exist
{
"code": 0,
"msg": "success",
"data": null
}
Transaction data structure
TransactionDetail returned by list queries
The /api/v1/transfer, /api/v1/transfer/multiple, and /api/v1/transfer/details APIs return this transaction format.
Main fields:
| Field | Type | Description |
|---|---|---|
chain_type | string | Normalized lowercase chain type. |
chain_id | integer | Chain ID. |
height | integer | null | Block height. |
tx_time | integer | null | Transaction time as a second-level Unix timestamp. |
tx_hash | string | Transaction hash. |
tx_version | integer | null | Transaction version, used by chains such as Solana. |
sender | string | null | Sender address. |
fee_payer | string | null | Fee payer address, mainly for Solana. |
receiver | string | null | Receiver address. |
transfer_type | string | null | Transaction type, such as native or token. |
amount | string | null | Transaction amount as a string to avoid precision loss. |
symbol | string | null | Native currency or token symbol, such as ETH or BNB. |
decimals | integer | null | Decimal places, such as 18. |
tx_status | string | null | Transaction status: success, failed, or pending. |
tx_fee | string | null | Transaction fee. |
gas_limit | string | null | Gas limit for EVM chains. |
gas_used | string | null | Actual gas used for EVM chains. |
gas_price | string | null | Gas price for EVM chains. |
max_fee_per_gas | string | null | EIP-1559 max fee per gas. |
max_priority_fee_per_gas | string | null | EIP-1559 max priority fee per gas. |
nonce | string | null | Account nonce for EVM chains. |
method_id | string | null | Contract method selector for EVM calls. |
method_call | string | null | Contract method name or call summary. |
l1_origin_hash | string | null | Original L1 transaction hash for L2 chains. |
token_transfers | array | Token transfer details. |
from_details | array | Input address details for UTXO-style data. |
to_details | array | Output address details for UTXO-style data. |
internal_transactions | array | Internal transactions or Solana instruction details. |
token_transfers:
| Field | Type | Description |
|---|---|---|
from_address | string | null | Token source address. |
to_address | string | null | Token destination address. |
token_contract_address | string | null | Token contract address. |
symbol | string | null | Token symbol. |
amount | string | null | Transfer amount. |
decimals | integer | null | Token decimals. |
is_from_contract | boolean | Whether the source address is a contract. |
is_to_contract | boolean | Whether the destination address is a contract. |
from_token_address | string | null | Source token account address for Solana. |
to_token_address | string | null | Destination token account address for Solana. |
mint | string | null | Solana mint address. |
program_id | string | null | Solana program ID. |
from_details, input details for UTXO-style data:
| Field | Type | Description |
|---|---|---|
address | string | null | Input address. |
amount | string | null | Input amount. |
is_contract | boolean | Whether the address is a contract. |
vin_index | string | null | UTXO input index. |
pre_vout_index | string | null | Previous transaction output index. |
ref_tx_hash | string | null | Referenced previous transaction hash. |
to_details, output details for UTXO-style data:
| Field | Type | Description |
|---|---|---|
address | string | null | Output address. |
amount | string | null | Output amount. |
is_contract | boolean | Whether the address is a contract. |
vout_index | string | null | UTXO output index. |
internal_transactions, internal calls or Solana instructions:
| Field | Type | Description |
|---|---|---|
from_address | string | null | Internal call sender. |
to_address | string | null | Internal call receiver. |
amount | string | null | Internal call amount. |
tx_status | string | null | Internal call status. |
program_id | string | null | Solana program ID. |
instr_index | integer | null | Solana instruction index. |
depth | integer | null | Solana call depth. |
instruction | string | null | Solana instruction name. |
description | string | null | Instruction description. |
V2 field naming differences
The /transaction/transaction/tx API returns a mixed naming style for historical compatibility:
| V2 field | Matching list field | Main difference |
|---|---|---|
txhash | tx_hash | Different field name. |
txTime | tx_time | camelCase and millisecond timestamp. |
txStatus | tx_status | camelCase. |
txFee | tx_fee | camelCase. |
gasLimit | gas_limit | camelCase. |
gasUsed | gas_used | camelCase. |
gasPrice | gas_price | camelCase. |
methodId | method_id | camelCase. |
methodCall | method_call | camelCase. |
l1OriginHash | l1_origin_hash | camelCase. |
fromDetails | from_details | camelCase. |
toDetails | to_details | camelCase. |
internalTransactionDetails | internal_transactions | Different field name. |
tokenTransferDetails | token_transfers | Different field name. |
Pagination
Transaction list queries use cursor pagination instead of page numbers:
- Do not pass
idin the first request. The service returns the latest N records. - If the returned record count equals
limit, there may be more data. - Use the database internal
idof the last returned record as the next request'sid. - Continue until the returned record count is smaller than
limit.
Cursor pagination does not support jumping to a page. It can only move forward sequentially.
Amount handling recommendations
All amount fields are returned as strings to avoid large-number precision loss. Convert display values with the decimals field:
function formatAmount(amount, decimals) {
if (!amount || decimals === null) return '0';
const bigAmount = BigInt(amount);
const divisor = BigInt(10 ** decimals);
const intPart = bigAmount / divisor;
const fracPart = bigAmount % divisor;
return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
}
// Example: 1000000000000000000 with decimals=18 becomes "1.000000000000000000".
console.log(formatAmount('1000000000000000000', 18));
5. Address management module
The address management module maintains the monitored address list for each business integration. Only addresses added to the list trigger transaction pushes to the corresponding WebSocket subscribers or Webhook callback.
API list
| Method | Path | Description |
|---|---|---|
| POST | /api/v1/address | Patch mode for adding and removing addresses in one request |
| POST | /api/v1/address/add | Batch add addresses |
| POST | /api/v1/address/remove | Batch remove addresses |
| POST | /api/v1/address/contains | Query whether an address exists in the list |
Public authentication notes
All protected address APIs identify the caller through X-API-Key. The service uses this key for API authentication, business ownership identification, and billing statistics.
| Method | Description | Example |
|---|---|---|
| Request header | Public API key | X-API-Key: your-api-key |
Public access only requires
X-API-Key. No additional application identity header or query parameter is required.
Add monitored addresses API
API path: POST /api/v1/address/add
Purpose
Batch adds addresses to the monitored address list for the current business identity. Multiple chains can be added in one request.
Chain-specific behavior
- Tron: Tron-format addresses starting with
Tare automatically converted to equivalent EVM-format0x...addresses for storage. - Other non-EVM chains: the service attempts to sync addresses to an internal platform monitoring component. Sync failures do not block local database writes.
Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
wallets | array | Yes | Address list grouped by chain. At least one item is required. |
wallets[].chain_type | string | Yes | Chain type, such as ethereum, solana, or tron. |
wallets[].address | string[] | Yes | Address array for the chain. At least one address is required. |
Example
# Add Ethereum addresses.
curl -X POST "$BASE_URL/api/v1/address/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"wallets": [
{
"chain_type": "ethereum",
"address": ["0x1234...", "0xabcd..."]
}
]
}'
# Add addresses for multiple chains.
curl -X POST "$BASE_URL/api/v1/address/add" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"wallets": [
{"chain_type": "ethereum", "address": ["0x1234..."]},
{"chain_type": "tron", "address": ["TJDENsfBJs4RFETt1X1uyT9pBxdnAbFnbm"]},
{"chain_type": "solana", "address": ["SolanaAddress..."]}
]
}'
Response
{
"code": 0,
"message": "ok",
"data": "success"
}
Remove monitored addresses
API path: POST /api/v1/address/remove
Purpose
Batch removes addresses from the monitored address list. After removal, new transactions for the address are no longer pushed to the current business identity.
Request body fields
Same as /api/v1/address/add: wallets[].chain_type plus wallets[].address.
Example
curl -X POST "$BASE_URL/api/v1/address/remove" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"wallets": [
{"chain_type": "ethereum", "address": ["0x1234..."]}
]
}'
Response
{
"code": 0,
"message": "ok",
"data": "success"
}
Patch address list
API path: POST /api/v1/address
Purpose
Uses patch mode to add and remove addresses for one chain in a single request.
Request body fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
chain_type | string | Yes | - | Chain type. This API operates on one chain at a time. |
adds | string[] | No | [] | Addresses to add to the list. |
removes | string[] | No | [] | Addresses to remove from the list. |
Example
curl -X POST "$BASE_URL/api/v1/address" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"chain_type": "ethereum",
"adds": ["0xNewAddress001...", "0xNewAddress002..."],
"removes": ["0xOldAddress001..."]
}'
Response
{
"code": 0,
"message": "ok",
"data": "success"
}
Address existence query
API path: POST /api/v1/address/contains
Purpose
Queries whether an address already exists in the address list for a specified chain.
In the current version, this API only checks whether the address exists in the global address table. It does not filter by API key isolation.
Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
chain_type | string | Yes | Chain type. |
address | string | Yes | Address to query. |
Example
curl -X POST "$BASE_URL/api/v1/address/contains" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"chain_type": "ethereum", "address": "0x1234..."}'
Response
{
"code": 0,
"message": "ok",
"data": {
"is_exist": true
}
}
Address management best practices
Address format guidelines:
- EVM chains: use hex addresses starting with
0x. EIP-55 checksum format is recommended. - Solana: use Base58 addresses.
- Tron: use Base58Check addresses starting with
T; the service automatically converts them to0xformat for storage. - Bitcoin: use standard Bitcoin address formats, such as P2PKH, P2SH, and Bech32.
Relationship between the address list and WebSocket subscriptions:
- Addresses added through
/api/v1/address/addare persisted in the database and remain effective after service restarts. - After a WebSocket connection is established, the service automatically monitors all addresses in the business identity's address list.
- You can also use the WebSocket
subscribecommand to add temporary monitoring after connection establishment, but this is only effective for the current connection and is not restored after disconnection.
For regular business monitoring, use HTTP APIs to manage the address list. Use WebSocket subscribe and unsubscribe only for temporary fine-grained control.
6. WebSocket real-time push
The WebSocket module provides real-time on-chain transaction push. After a client establishes a WebSocket connection, the service immediately pushes messages when monitored addresses produce on-chain transactions.
Connection endpoint:
GET /transaction/transaction/ws
Establishing a connection
Request notes
The WebSocket handshake requires these request headers:
| Header | Type | Required | Description |
|---|---|---|---|
Upgrade | string | Yes | Fixed value: websocket. |
X-API-Key | string | Yes | Public API key for authentication and billing identification. |
X-ResendDuration | string | No | Historical message resend window in seconds. Default: 180. |
If
X-API-Keyis missing, the connection request returns400or an authentication failure.
Connection example
# Use wscat.
wscat -c "wss://api.gelabs.org/transaction/transaction/ws" \
-H "X-API-Key: your-api-key"
Successful connection message
After the connection is established, the server immediately pushes:
{
"id": "msg-uuid-001",
"time": 1745000000000,
"type": "connected",
"code": 0,
"data": {
"clientId": "client-abc123",
"appId": "current-business-identity"
},
"msg": "success"
}
| Field | Description |
|---|---|
data.clientId | Unique server-side identifier of the current connection. It changes after reconnecting. |
data.appId | Caller identity bound to the current connection. |
Connection failure
| HTTP status | Reason | Recommendation |
|---|---|---|
400 | API key not provided. | Confirm that X-API-Key is passed correctly. |
426 | Upgrade: websocket header is missing. | Confirm that the client connects through the WebSocket protocol. |
Message format
All WebSocket messages use JSON and follow this envelope:
{
"id": "message unique ID",
"time": 1745000000000,
"type": "message type",
"code": 0,
"data": {},
"msg": "success"
}
| Field | Type | Description |
|---|---|---|
id | string | Unique message ID generated by the sender. |
time | integer | Message timestamp as a millisecond Unix timestamp. |
type | string | Message type that determines business meaning. |
code | integer | Business status code. 0 means success and non-zero means error. |
data | object | null | Business data. |
msg | string | Text message. Successful responses usually use success. |
Client commands
Clients can send the commands below to the server. Each command needs a unique id, and the server response uses the same id.
ping (heartbeat)
Sends a heartbeat to keep the connection active.
Send:
{
"id": "ping-001",
"type": "ping",
"data": {}
}
Server response:
{
"id": "ping-001",
"time": 1745000000000,
"type": "pong",
"code": 0,
"data": null,
"msg": "success"
}
subscribe (subscribe to an address)
Subscribes to real-time transaction pushes for a specified address on a specified chain.
Addresses subscribed through this command are only effective for the current connection and are not saved after disconnection. For persistent address subscription, add the address through the address management module in advance.
Send:
{
"id": "cmd-001",
"type": "subscribe",
"data": {
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain_type": "ethereum"
}
}
Command parameters:
| Field | Type | Required | Description |
|---|---|---|---|
data.address | string | Yes | Address to subscribe. |
data.chain_type | string | Yes | Chain type. The server converts it to lowercase. |
Success response:
{
"id": "cmd-001",
"type": "subscribe",
"code": 0,
"data": {"API Key": "current-business-identity", "address": "0x1234...", "chain_type": "ethereum"},
"msg": "success"
}
Error response:
{
"id": "cmd-001",
"type": "subscribe",
"code": 400,
"data": null,
"msg": "address and chain_type required"
}
unsubscribe (unsubscribe)
Cancels the real-time transaction subscription for an address.
Send for a specific chain:
{
"id": "cmd-002",
"type": "unsubscribe",
"data": {
"address": "0x1234...",
"chain_type": "ethereum"
}
}
Send for all chains by omitting chain_type:
{
"id": "cmd-002",
"type": "unsubscribe",
"data": {
"address": "0x1234..."
}
}
Command parameters:
| Field | Type | Required | Description |
|---|---|---|---|
data.address | string | Yes | Address to unsubscribe. |
data.chain_type | string | No | Chain type. If omitted, all chain subscriptions for this address are canceled. |
Success response:
{
"id": "cmd-002",
"type": "unsubscribe",
"code": 0,
"data": {"address": "0x1234...", "chainType": "ethereum"},
"msg": "success"
}
When all chains are canceled, chainType is null.
getTxHash (query transaction details)
Queries the full details of one transaction through the WebSocket connection.
Send:
{
"id": "cmd-003",
"type": "getTxHash",
"data": {
"chain_type": "ethereum",
"chain_id": 1,
"txHash": "0xabc123..."
}
}
Command parameters:
| Field | Type | Required | Description |
|---|---|---|---|
data.chain_type | string | Yes | Chain type. |
data.chain_id | integer | Yes | Chain ID. |
data.txHash | string | No | Transaction hash. |
data.messageId | string | No | Unique ID of the real-time push message. Prefer this field when available. |
Prefer
messageId, theidvalue of the push message, so the server can locate the record more accurately.
Error response when the transaction does not exist:
{
"id": "cmd-003",
"type": "getTxHash",
"code": 404,
"data": null,
"msg": "transaction not found"
}
transactionACK (confirm processed transaction)
After receiving and processing a transaction message, the client should send an ACK. After receiving the ACK, the server stops retrying that transaction.
If the client does not send ACK for a long time, the server may resend unacknowledged transactions after the client reconnects to prevent message loss.
Send:
{
"id": "ack-001",
"type": "transactionACK",
"data": {
"chain_type": "ethereum",
"chain_id": 1,
"txHash": "0xabc123..."
}
}
Success response:
{
"id": "ack-001",
"type": "transactionACK",
"code": 0,
"data": {"txHash": "0xabc123..."},
"msg": "success"
}
Server push messages
transaction (real-time transaction push)
When a monitored address has an on-chain transaction, the server pushes:
{
"id": "msg-tx-uuid-001",
"time": 1745000000000,
"type": "transaction",
"code": 0,
"data": {
"chain_type": "ethereum",
"chain_id": 1,
"height": 21000000,
"txTime": 1745000000000,
"txhash": "0xabc123...",
"sender": "0x1234...",
"fee_payer": "0x1234...",
"receiver": "0x5678...",
"transfer_type": "native",
"gasLimit": "21000",
"gasUsed": "21000",
"gasPrice": "20000000000",
"max_fee_per_gas": "30000000000",
"max_priority_fee_per_gas": "1000000000",
"txFee": "420000000000000",
"nonce": "42",
"symbol": "ETH",
"decimals": 18,
"amount": "1000000000000000000",
"txStatus": "success",
"methodId": "",
"methodCall": "",
"fromDetails": [],
"toDetails": [],
"internalTransactionDetails": null,
"tokenTransferDetails": null,
"listenAddress": ["0x1234..."],
"is_reorg_resend": false
},
"msg": "success"
}
Key transaction message fields:
| Field | Type | Description |
|---|---|---|
data.listenAddress | string[] | Addresses that matched monitoring rules. One transaction may match multiple addresses. |
data.is_reorg_resend | boolean | Whether this is a resend after a chain reorganization. If true, check local records. |
data.tokenTransferDetails | array | null | Token transfer details. Present for ERC-20 transfers. |
data.txTime | integer | Transaction timestamp in milliseconds. |
is_reorg_resend notes:
When the value is true, the transaction is resent because of a chain reorganization. Check whether the local record for the same txhash already exists and update fields such as block height when needed.
ERC-20 token transfer message example:
{
"id": "msg-tx-002",
"type": "transaction",
"code": 0,
"data": {
"chain_type": "ethereum",
"txhash": "0xdef456...",
"transfer_type": "token",
"tokenTransferDetails": [
{
"from": "0x1234...",
"to": "0x5678...",
"isFromContract": false,
"isToContract": false,
"tokenContractAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"symbol": "USDT",
"amount": "1000000000",
"decimals": 6,
"fromTokenAddress": "",
"toTokenAddress": "",
"mint": "",
"programId": ""
}
],
"listenAddress": ["0x1234..."],
"is_reorg_resend": false
},
"msg": "success"
}
HTTP helper APIs
WebSocket connection status query
API path: GET /transaction/transaction/ws/status
Queries the WebSocket connection status for the current business identity.
curl "$BASE_URL/transaction/transaction/ws/status" \
-H "X-API-Key: your-api-key"
The caller identity is identified by X-API-Key. Public access does not require an additional application identity query parameter.
Response:
{
"code": 0,
"message": "ok",
"data": {
"appId": "current-business-identity",
"connectedClients": 2,
"clients": ["client-abc123", "client-def456"],
"details": [
{
"clientId": "client-abc123",
"connectedAt": 1745000000000,
"uptime": 3600,
"pendingTxCount": 0
}
]
}
}
| Field | Type | Description |
|---|---|---|
connectedClients | integer | Number of online clients. |
details[].uptime | integer | Lifetime in seconds. |
details[].pendingTxCount | integer | Number of pending transactions that have not received ACK. |
WebSocket message replay
API path: POST /transaction/transaction/ws/replay
Manually replays a missed transaction message. The server will:
- Idempotently write the message into the database.
- Push it to currently online WebSocket subscribers.
- Trigger Webhook callbacks in parallel.
The request body is a complete WsTransactionMessage, the same format as the server-pushed transaction message.
Response:
{
"code": 0,
"message": "ok",
"data": {
"processed": true,
"txhash": "0xabc123..."
}
}
Error handling
WebSocket command errors use the same JSON envelope and set code to a non-zero value:
{
"id": "original command id",
"type": "command type",
"code": 400,
"data": null,
"msg": "error description"
}
Common errors:
| Command | code | msg |
|---|---|---|
subscribe | 400 | address and chain_type required |
unsubscribe | 400 | address required |
getTxHash | 400 | chain_type, chain_id, txHash required |
getTxHash | 404 | transaction not found |
transactionACK | 400 | chain_type and messageId/txHash required |
Best practices
Heartbeat
Send a ping command every 30 seconds:
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ id: `ping-${Date.now()}`, type: 'ping', data: {} }));
}
}, 30000);
Reconnect after disconnection
function connect() {
const ws = new WebSocket(WS_URL, { headers: { 'X-API-Key': API_KEY } });
ws.on('close', () => {
console.log('Connection closed, reconnecting in 5 seconds...');
setTimeout(connect, 5000);
});
return ws;
}
ACK strategy
- ACK promptly: after receiving and processing a
transactionmessage, sendtransactionACKimmediately. - Avoid missing ACK: if business processing is slow, consider sending ACK first and processing asynchronously.
- Deduplicate ACKs: use
txhash + chain_type + chain_idas the business idempotency key.
Address subscription strategy
| Method | Persistence | Use case |
|---|---|---|
HTTP address management (/api/v1/address/add) | Persistent, effective after restart | Long-term monitoring of regular business addresses |
WebSocket subscribe command | Temporary, only effective for the current connection | Temporary debugging or dynamic fine-grained control |
Concurrent connections
The business identity represented by the same API key supports multiple clients connected at the same time. The server broadcasts transaction messages to all online connections. This is suitable for horizontal scaling or active-standby switching, as long as the business layer handles messages idempotently.
Complete integration example
const WebSocket = require('ws');
const WS_URL = 'wss://api.gelabs.org/transaction/transaction/ws';
const API_KEY = 'your-api-key';
const processedTxHashes = new Set(); // Idempotency tracking.
function connect() {
const ws = new WebSocket(WS_URL, {
headers: { 'X-API-Key': API_KEY }
});
let pingInterval;
ws.on('open', () => {
console.log('WebSocket connection established');
pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ id: `ping-${Date.now()}`, type: 'ping', data: {} }));
}
}, 30000);
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
switch (msg.type) {
case 'connected':
console.log(`Connection confirmed: appId=${msg.data.appId}, clientId=${msg.data.clientId}`);
break;
case 'pong':
break;
case 'transaction': {
if (msg.code !== 0) {
console.error('Error message received:', msg.msg);
break;
}
const tx = msg.data;
const txKey = `${tx.chain_type}-${tx.chain_id}-${tx.txhash}`;
if (!processedTxHashes.has(txKey)) {
processedTxHashes.add(txKey);
console.log(`New transaction: chain=${tx.chain_type}, hash=${tx.txhash}, amount=${tx.amount} ${tx.symbol}`);
if (tx.is_reorg_resend) {
console.warn(`Reorg resend: ${tx.txhash}. Check whether local records need updates.`);
}
if (tx.tokenTransferDetails && tx.tokenTransferDetails.length > 0) {
tx.tokenTransferDetails.forEach((t) => {
console.log(` Token transfer: ${t.amount} ${t.symbol} from ${t.from} to ${t.to}`);
});
}
}
// Send ACK after receiving the transaction.
ws.send(JSON.stringify({
id: `ack-${msg.id}`,
type: 'transactionACK',
data: { chain_type: tx.chain_type, chain_id: tx.chain_id, txHash: tx.txhash },
}));
break;
}
default:
console.log(`Unknown message type: ${msg.type}`);
}
});
ws.on('close', (code) => {
clearInterval(pingInterval);
console.log(`Connection closed (code=${code}), reconnecting in 5 seconds...`);
setTimeout(connect, 5000);
});
ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
});
}
connect();