Never miss an important transaction on TRON — without running your own node.
Tronvent is a self-hosted TRON chain monitor. Register the wallet addresses and TRC-20 contracts you care about, and Tronvent polls TronGrid on your behalf, filters every block for relevant activity, and delivers signed webhooks to your application in near real time.
Building reliable TRON deposit/withdrawal detection is harder than it looks:
-
TronGrid has no push subscription. The hosted TronGrid API is REST-only. There is no WebSocket or server-sent event stream to subscribe to transactions or contract events. The official TRON docs describe monitoring addresses by continuously polling account history APIs. TronGrid maintainers have confirmed there is no WebSocket API; real-time push requires running your own full node with ZeroMQ event subscription — infrastructure most teams do not want to operate.
-
The chain moves fast. TRON produces a new block roughly every 3 seconds. To catch inbound TRX transfers you must scan every new block. For TRC-20 tokens you additionally query contract event indexes, which can lag block data by several seconds.
-
Scale adds up quickly. A wallet or exchange may watch thousands or millions of deposit addresses. Polling per-address APIs does not scale. Scanning full blocks and filtering locally is the practical approach — but someone still has to run that loop reliably, persist cursors, handle restarts, and deliver events to your backend.
Tronvent exists to do exactly that: one service that watches your addresses and contracts, keeps up with the chain, and pushes matched events to you via webhooks.
| You provide | Tronvent handles |
|---|---|
| TRON addresses to watch | Polls TronGrid every ~3 s for new blocks |
| TRC-20 contracts (e.g. USDT) | Fetches Transfer events per contract |
| A webhook URL + signing secret | Delivers signed JSON payloads with retries |
| PostgreSQL + TronGrid API key | Persists cursors, outbox, and watchlists |
Supported activity today:
- TRX transfers — native TRX
TransferContracttransactions where your address is sender or receiver - TRC-20 transfers —
Transferevents on watched token contracts (USDT is seeded by default)
When a match is found, Tronvent writes to a durable Postgres outbox and a background worker POSTs a signed webhook to your endpoint.
flowchart LR
TG[TronGrid API]
P[Block Poller]
BF[Bloom Filter]
PG[(PostgreSQL)]
W[Webhook Worker]
APP[Your Application]
TG -->|blocks + TRC-20 events| P
P -->|Contains?| BF
P -->|matched events| PG
PG -->|outbox| W
W -->|signed POST| APP
APP -->|admin API| PG
PG -->|LISTEN/NOTIFY| BF
Every TRON_POLL_INTERVAL_MS (default 3000 ms, aligned with TRON block time):
- Fetch chain tip —
GET /wallet/getnowblockto learn the latest block height. - Apply confirmation lag — only scan blocks up to
latest − TRON_REQUIRED_CONFIRMATIONSso events are not published from blocks that might still reorganize. - Scan TRX — batch-fetch blocks via
/wallet/getblockbylimitnext(up to 100 blocks per request), inspect every transaction, and matchTransferContractsenders/receivers against your watchlist. - Scan TRC-20 — for each watched contract, query
/v1/contracts/{address}/eventswith fingerprint pagination. An extraTRON_TRC20_EVENT_CONFSoffset accounts for TronGrid's asynchronous event indexing. - Advance cursors — each scope (
TRX, plus one cursor per TRC-20 contract) stores its highest scanned block in Postgres. On restart, scanning resumes from the last committed block. - Enqueue webhooks — matched events are deduplicated and written to
webhook_events. The delivery worker claims pending rows and POSTs to your URL.
TRX and each TRC-20 contract scan concurrently with independent cursors, so one slow contract cannot block native TRX detection.
Every watched address is held in an in-memory Bloom filter — a probabilistic data structure that answers “is this address probably in my set?” in O(1) time with minimal memory.
Tronvent tunes the filter for 1 million addresses at a 0.1% false-positive rate (~14 MB of bit storage, 10 hash functions). Properties:
| Property | Behavior |
|---|---|
| No false negatives | A real watched address is never missed. |
| Rare false positives | ~1 in 1,000 non-watched addresses may pass the filter. These are harmless: the event is still deduplicated and delivered; you simply receive a webhook you can ignore. |
| Memory efficient | Millions of addresses fit in a few megabytes instead of a multi-gigabyte hash map. |
The filter reloads from Postgres on startup, on LISTEN/NOTIFY when addresses change via the admin API, and on a periodic safety-net interval (STATE_RESYNC_INTERVAL_SECONDS).
- Postgres outbox — events are persisted before delivery; crashes do not lose matches.
- Webhook retries — exponential backoff (1 m → 2 h) up to
WEBHOOK_MAX_ATTEMPTS(default 8). Every attempt is logged inwebhook_delivery_attempts. - Startup reconciliation — if the scanner was offline and fell far behind, large gaps are enqueued as background block-range jobs instead of blocking the live poll loop.
- Admin replay — manually re-scan a block or range via the admin API when you need to backfill.
- Prometheus metrics —
/metricsexposes blocks scanned, matches found, events published, and watchlist size.
With tuned settings Tronvent stays within a few blocks of chain tip and delivers webhooks shortly after your confirmation threshold:
| Setting | Default | Low-latency example |
|---|---|---|
TRON_POLL_INTERVAL_MS |
3000 |
3000 |
TRON_REQUIRED_CONFIRMATIONS |
20 |
5 |
WEBHOOK_POLL_INTERVAL_MS |
1000 |
1000 |
At 5 confirmations and a 3 s block time, a transaction is eligible for scanning ~15 s after inclusion. The next poll tick and webhook dispatch add a few more seconds — typically under 30 seconds from the confirmation you configured.
The default of 20 confirmations trades latency for stronger finality (~60 s of block time). Adjust based on your risk tolerance.
Tronvent is designed to handle thousands to millions of watched addresses and the full transaction volume of each block without falling more than your configured confirmation depth behind tip under normal TronGrid rate limits.
- PostgreSQL 16+ with the
pgcryptoextension (forgen_random_uuid()) - TronGrid API key — required for mainnet; get one free at trongrid.io. Shasta and Nile testnets currently work without a key but setting one is recommended.
- Go 1.26+ (local builds only)
Choose a deployment path below. All container-based paths use published artifacts; you do not need to clone this repository.
Create a docker-compose.yml file with this sample:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: tronvent
POSTGRES_USER: tronvent
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-tronvent-local}
volumes:
- tronvent-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tronvent -d tronvent"]
interval: 5s
timeout: 5s
retries: 10
migrate:
image: ghcr.io/degoke/tronvent:1.0.0
command: ["/app/migrate"]
environment:
DATABASE_URL: postgres://tronvent:${POSTGRES_PASSWORD:-tronvent-local}@postgres:5432/tronvent
depends_on:
postgres:
condition: service_healthy
tronvent:
image: ghcr.io/degoke/tronvent:1.0.0
ports:
- "8080:8080"
env_file: .env
environment:
DATABASE_URL: postgres://tronvent:${POSTGRES_PASSWORD:-tronvent-local}@postgres:5432/tronvent
depends_on:
migrate:
condition: service_completed_successfully
volumes:
tronvent-postgres:Then create a .env file with the required values:
TRONGRID_API_KEY_SCANNER=your-trongrid-api-key
ADMIN_API_TOKEN=a-long-random-secret
WEBHOOK_URL=https://your-app.example.com/webhooks/tron
WEBHOOK_SIGNING_SECRET=another-long-random-secretThen start the complete local stack:
TRONVENT_IMAGE=ghcr.io/degoke/tronvent:1.0.0 docker compose up -d
docker compose logs -f tronventTronvent is available at http://localhost:8080. Stop the stack with docker compose down; add -v if you also want to remove the local PostgreSQL volume.
docker pull ghcr.io/degoke/tronvent:1.0.0
docker run --rm -p 8080:8080 \
-e DATABASE_URL="$DATABASE_URL" \
-e TRONGRID_API_KEY_SCANNER="$TRONGRID_API_KEY_SCANNER" \
-e TRONGRID_BASE_URL="https://api.trongrid.io" \
-e ADMIN_API_TOKEN="$ADMIN_API_TOKEN" \
-e WEBHOOK_URL="$WEBHOOK_URL" \
-e WEBHOOK_SIGNING_SECRET="$WEBHOOK_SIGNING_SECRET" \
ghcr.io/degoke/tronvent:1.0.0Pre-built images are published to ghcr.io/degoke/tronvent on release.
This route assumes PostgreSQL and migrations are already available. For a complete PostgreSQL + migration + Tronvent stack, use Docker Compose above.
Install a released chart from GHCR:
helm registry login ghcr.io
helm upgrade --install tronvent oci://ghcr.io/degoke/charts/tronvent \
--version 1.0.0 \
--namespace tronvent --create-namespace \
--set secrets.databaseUrl="$DATABASE_URL" \
--set secrets.tronGridApiKey="$TRONGRID_API_KEY_SCANNER" \
--set secrets.adminApiToken="$ADMIN_API_TOKEN" \
--set secrets.webhookSigningSecret="$WEBHOOK_SIGNING_SECRET" \
--set config.tronGridBaseUrl="https://api.trongrid.io" \
--set config.webhookUrl="$WEBHOOK_URL"The release workflow publishes both the Docker image and Helm chart when a v* tag is pushed. The chart is available at oci://ghcr.io/degoke/charts/tronvent.
# Run migrations first (see above)
helm upgrade --install tronvent ./charts/tronvent \
--namespace tronvent --create-namespace \
--set secrets.databaseUrl="$DATABASE_URL" \
--set secrets.tronGridApiKey="$TRONGRID_API_KEY_SCANNER" \
--set secrets.adminApiToken="$ADMIN_API_TOKEN" \
--set secrets.webhookSigningSecret="$WEBHOOK_SIGNING_SECRET" \
--set config.tronGridBaseUrl="https://api.trongrid.io" \
--set config.webhookUrl="$WEBHOOK_URL"For production, create a Kubernetes Secret ahead of time and reference it:
secrets:
create: false
existingSecret: tronvent-secretsSee charts/tronvent/values.yaml for all configurable values including resource limits, probes, ingress, and Prometheus ServiceMonitor.
make migrateexport DATABASE_URL="postgres://user:pass@localhost:5432/tronvent"
export TRONGRID_API_KEY_SCANNER="your-trongrid-api-key"
export ADMIN_API_TOKEN="a-long-random-secret"
export WEBHOOK_URL="https://your-app.example.com/webhooks/tron"
export WEBHOOK_SIGNING_SECRET="another-long-random-secret"
# Network — pick one:
export TRONGRID_BASE_URL="https://api.trongrid.io" # Mainnet
# export TRONGRID_BASE_URL="https://api.shasta.trongrid.io" # Shasta testnet
# export TRONGRID_BASE_URL="https://nile.trongrid.io" # Nile testnetgit clone https://github.com/degoke/tronvent.git
cd tronvent
make build
./bin/tronventOr with live reload during development:
air # requires github.com/air-verse/airBASE=http://localhost:8080
AUTH="Authorization: Bearer $ADMIN_API_TOKEN"
# Watch a deposit address
curl -s -X POST "$BASE/api/v1/addresses" \
-H "$AUTH" -H "Content-Type: application/json" \
-d '{"address":"TXYZ..."}'
# Watch a TRC-20 contract
curl -s -X POST "$BASE/api/v1/contracts" \
-H "$AUTH" -H "Content-Type: application/json" \
-d '{"contractAddress":"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t","tokenSymbol":"USDT"}'
# Configure webhook delivery
curl -s -X PUT "$BASE/api/v1/webhook" \
-H "$AUTH" -H "Content-Type: application/json" \
-d "{\"webhookUrl\":\"$WEBHOOK_URL\",\"signingSecret\":\"$WEBHOOK_SIGNING_SECRET\"}"
# Check scanner state
curl -s "$BASE/api/v1/runtime" -H "$AUTH" | jqHealth check: GET /health
Metrics: GET /metrics
| Network | TRONGRID_BASE_URL |
Notes |
|---|---|---|
| Mainnet | https://api.trongrid.io |
API key required |
| Shasta testnet | https://api.shasta.trongrid.io |
Faucet |
| Nile testnet | https://nile.trongrid.io |
Faucet |
Set TRONGRID_BASE_URL to match the network your addresses and contracts live on. Use separate Tronvent deployments (and databases) per network.
Reference: TRON network endpoints
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
TRONGRID_API_KEY_SCANNER |
TronGrid API key (TRON-PRO-API-KEY header) |
| Variable | Default | Description |
|---|---|---|
TRONGRID_BASE_URL |
https://api.trongrid.io |
TronGrid HTTP endpoint |
TRON_POLL_INTERVAL_MS |
3000 |
Poll tick interval (ms) |
TRON_START_BLOCK |
0 |
Start block; 0 = resume cursor or begin at current tip |
TRON_REQUIRED_CONFIRMATIONS |
20 |
Blocks to wait before scanning (finality vs latency) |
TRON_TRC20_EVENT_CONFS |
10 |
Extra lag for TRC-20 event index |
TRON_TRC20_CURSOR_RETAIN |
50 |
Re-scan recent blocks while event index catches up |
TRON_FETCH_CONCURRENCY |
5 |
Parallel TronGrid HTTP requests |
TRON_HTTP_TIMEOUT_SECONDS |
60 |
HTTP client timeout |
TRON_RECONCILE_BATCH_SIZE |
1000 |
Blocks per startup backfill job |
TRON_TRC20_CONTRACTS |
USDT mainnet | Comma-separated contracts to seed on first boot |
| Variable | Default | Description |
|---|---|---|
WEBHOOK_URL |
— | Bootstrap webhook URL (overridden by admin API) |
WEBHOOK_SIGNING_SECRET |
— | HMAC-SHA256 signing secret |
WEBHOOK_MAX_ATTEMPTS |
8 |
Max delivery attempts per event |
WEBHOOK_POLL_INTERVAL_MS |
1000 |
Outbox poll interval |
WEBHOOK_HTTP_TIMEOUT_SECONDS |
30 |
Delivery HTTP timeout |
| Variable | Default | Description |
|---|---|---|
HEALTH_PORT |
8080 |
HTTP port (health, metrics, admin API) |
ADMIN_API_TOKEN |
— | Bearer token for /api/v1/* (required for admin API) |
STATE_RESYNC_INTERVAL_SECONDS |
60 |
Periodic watchlist reload safety net |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARN, ERROR |
LOG_FORMAT |
auto | json or text (color when TTY) |
All /api/v1/* routes require Authorization: Bearer <ADMIN_API_TOKEN>.
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/addresses |
Add watched address |
GET |
/api/v1/addresses |
List addresses (?status=active&limit=50) |
DELETE |
/api/v1/addresses/{address} |
Deactivate address |
POST |
/api/v1/contracts |
Add watched TRC-20 contract |
GET |
/api/v1/contracts |
List contracts |
DELETE |
/api/v1/contracts/{contractAddress} |
Deactivate contract |
PUT |
/api/v1/webhook |
Set webhook URL and signing secret |
GET |
/api/v1/webhook |
Get webhook config (secret not returned) |
GET |
/api/v1/runtime |
Cursors, watchlist counts, active contracts |
POST |
/api/v1/retries/block |
Replay a single block |
POST |
/api/v1/retries/range |
Replay a block range |
GET |
/api/v1/retries |
List retry jobs |
Address and contract changes propagate to the in-memory Bloom filter immediately via Postgres NOTIFY — no restart required.
Each delivery is a POST with JSON body and signed headers:
X-Tronvent-Event-Id: <uuid>
X-Tronvent-Event-Type: TRX | TRC20
X-Tronvent-Timestamp: <unix seconds>
X-Tronvent-Signature: sha256=<hex hmac>
Content-Type: application/json
Body example:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "TRC20",
"txHash": "abc123...",
"fromAddress": "TXyz...",
"toAddress": "TAbc...",
"amount": "1000000",
"tokenContractAddress": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"blockNumber": 65432100,
"blockTimestamp": 1719234567000,
"confirmations": 5
}For TRC-20, amount is the raw token value (check contract decimals). For TRX, it is a decimal string in TRX units.
The signature is HMAC-SHA256(secret, timestamp + "." + raw_body):
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(strconv.FormatInt(timestamp, 10)))
mac.Write([]byte("."))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))Reject requests where the timestamp is too old (replay protection) or the signature does not match.
make help # list targets
make test # run tests
make lint # golangci-lint
make check # fmt + vet + lint + test + build
make docker # build local image┌─────────────┐ poll blocks/events ┌──────────────┐
│ TronGrid │ ◄────────────────────────── │ Poller │
│ (hosted) │ │ + BloomFilter│
└─────────────┘ └──────┬───────┘
│ matched events
▼
┌──────────────┐
│ PostgreSQL │
│ cursors + │
│ outbox │
└──────┬───────┘
│
┌─────────────────────────────┼──────────────────────────┐
▼ ▼ ▼
Webhook Worker Admin API (:8080) LISTEN/NOTIFY
(signed POST) addresses / contracts live reload
│
▼
Your application
You can — and for a handful of addresses the per-account history APIs work fine. Tronvent becomes worthwhile when you need:
- Many addresses — block-level scanning + Bloom filter beats N separate account pollers
- Both TRX and TRC-20 — unified cursors, deduplication, and webhook delivery
- Operational guarantees — crash-safe outbox, gap reconciliation, metrics, replay tools
- No node ops — TronGrid handles chain access; you run one small Go service + Postgres
The alternative — running a TRON full node with ZeroMQ event subscription — gives true push semantics but requires syncing and maintaining node infrastructure. Tronvent is the middle path: hosted chain access, self-hosted reliability.
