Tron energy marketplace
TRON energy (+ bandwidth) marketplace — match ~4/s · ~50ms platform-side; ~1k orders/day · ~60k TRX/day; peak TVL ~$45M (staked TRX delegated via market, Jan–Aug 2026). Correctness over CEX-scale throughput.
- Node.js
- TypeScript
- Kafka
- BullMQ
- MongoDB
- Redis
- Web3 / blockchain
- Fastify
- GraphQL
- Apollo
- Telegram
- API design
Intro
Marketplace that connects buyers who need TRON energy or bandwidth with sellers/providers who delegate resource on-chain. Buyer places an order, deposits TRX, matching fills against provider inventory, then DelegateResource lands on chain; providers sell auto or manual; reclaim undelegates when the lock expires; payout closes the loop. The API edge exposes both REST (Fastify) and GraphQL (Apollo) for the order lifecycle. Same ownership window also covers flash-unstake, vote, referral, and a Telegram path that is almost only chain-core (user-owned, anti-dup — nearly no UI/wiring).
Architecture
- 01Buyers / APIorder · estimate · deposit
- 02Providersauto-sell · manual delegate
- 03Telegram chain-coreon-chain intents · anti-dup · nearly no UI
- 04API serviceFastify REST + Apollo GraphQL · order lifecycle
- 05Matchingfill · DelegateResource · ~4/s · ~50ms platform-side
- 06Deposit consumerKafka · TRX credit path
- 07BullMQ workersdeposit · withdraw · on-chain settle
- 08Cronsreclaim · payout · flash-unstake · vote · referral
- 09TRONdelegate / undelegate · ~3s block · confirm separate
What I did
- Built the API edge (Fastify REST + Apollo GraphQL), matching, and deposit consume-tx from scratch, then handed the surface to the team — kept ownership of correctness boundaries (commit-before-broadcast, no blind rollback on ambiguous broadcast)
- Owned BullMQ queues + workers from scratch for deposit, withdraw, and on-chain settle — Kafka still credits TRX on the deposit path; BullMQ handles the complementary async jobs
- Owned crons — reclaim/undelegate on expiry, payout, flash-unstake release paths, vote/referral reward loops
- Scoped Telegram work to chain-core only (user-owned intents, anti-dup) — nearly no bot UI or product wiring claimed
- Led a team of ~4 backend on this product; platform-side match ~4/s · ~50ms under constrained volume (~1k orders/day · ~60k TRX/day); peak TVL ~$45M (staked TRX delegated via market, Jan–Aug 2026); product uptime 99.9% (this product — may share the number with trading, stated per product)
Hard problems
Broadcast timeout / DUP → double-delegate
- Symptom
- Ambiguous broadcast (HTTP timeout or DUP_TRANSACTION_ERROR) treated as hard fail → blind rollback → next tick signs a new delegate → provider locked twice, buyer over-filled.
- Cause
- Timeout and DUP are ambiguous — the tx may already be in mempool or on chain. Rollback without on-chain recheck invents a second intent.
- Fix
- Persist commit/sentinel before broadcast; route timeout/DUP into recheck-by-txid instead of immediate rollback. Public bar is correctness (no double-delegate), not CEX fill rate.
Serial matching correctness vs drain
- Symptom
- Pushing match throughput harder risked overlapping fills on the same provider inventory — over-delegate or unpaid lock.
- Cause
- Fill path mutates shared delegatable state; parallel drain without a serial correctness boundary races remainAmount / energyDelegatable.
- Fix
- Kept matching serial where money and resource locks meet; accepted ~4/s platform-side fill as the honest band under that constraint. Chain confirm stays bound by ~3s block time — separate from the ~50ms internal match path.
Reclaim, deposit, and false-success on chain paths
- Symptom
- Users (or ops) could see “done” while undelegate, Kafka deposit credit, or payout was still ambiguous — disputes look like lost TRX or stuck resource.
- Cause
- Expiry reclaim, deposit consume, and payout are multi-step; trusting broadcast result alone (or skipping recheck) creates false success-type signals.
- Fix
- Crons reclaim on lock expiry from authoritative state; deposit path credits via Kafka consumer with durable handling; BullMQ workers own deposit / withdraw / on-chain settle jobs; chain paths wait on recheck before user-facing success. No finer dispute-rate published.
Problem
Buyers needed TRON energy/bandwidth without running their own stake; providers had delegatable resource to sell. The product is a marketplace — order → deposit → match → on-chain delegate — plus reclaim, payout, and adjacent flash-unstake / vote / referral flows. Async paths split cleanly: Kafka credits TRX after durable deposit commits; BullMQ queues + workers (built from scratch) handle deposit, withdraw, and on-chain settle. Throughput is modest; the hard bar is not double-delegating and not lying about chain outcomes.
Trade-offs
- Correctness over drain — matching stays serial at the money/resource boundary. ~4 fills/s and ~50ms platform-side latency are the published band; they are not on-chain confirm times (TRON block ~3s; delegate confirm is separate).
- Commit-before-broadcast — persist intent/sentinel before broadcast so timeout/DUP can recheck instead of rolling back blind. Fail-closed (stuck pending, manual clear) is acceptable on money paths.
- Telegram as chain-core only — almost no UI/wiring claimed; ownership is anti-dup and on-chain intent handling. scan-delegate existed historically and was dropped — not presented as a live component.
- Complementary async — Kafka consumer credits TRX on the deposit path; BullMQ owns deposit / withdraw / on-chain settle jobs (queues + workers from scratch). Neither replaces the other.
- Hand-off after build — Fastify REST + Apollo GraphQL API / matching / consume-tx were built from scratch then transferred to the team; crons stayed owned.
Outcomes
Matching/fill ~4/s · ~50ms platform-side (internal match path). Volume ~1k orders/day · ~60k TRX/day under real constraints. Peak TVL ~$45M (staked TRX delegated via market, Jan–Aug 2026). Product uptime 99.9% (this product). Numbers stay small on purpose — correctness (no double-delegate, commit-before-broadcast) mattered more than CEX-scale throughput. Client names and finer failure rates stay internal.