stream — reconnecting real-time data
Every stream returns a Stream<T>: an event emitter AND a backpressure-safe
async iterable, backed by the same source. Log-based streams (streamSwaps,
streamLaunches, streamPortfolio) use a gap-fill cursor — it only
advances after a block range's logs are fully delivered, so a dropped RPC connection re-scans
the missed range on the next poll instead of silently skipping it.
import { createHoodClient } from 'hoodchain'
import { streamPrices, streamSwaps, streamLaunches } from 'hoodkit'
const hood = createHoodClient()
// Event-emitter form
const prices = streamPrices(hood, ['AAPL', 'TSLA'])
prices.on('data', (tick) => console.log(tick.symbol, tick.priceUsd))
prices.on('error', (err) => console.error('feed error', err))
// later: prices.close()
// Async-iterator form — same stream, different consumption style
const swaps = await streamSwaps(hood, { token: '0x…WEN' })
for await (const swap of swaps) {
console.log(swap.buysToken0 ? 'BUY' : 'SELL', swap.price, swap.volume1)
}
// Launches, gap-filled from a historical block if you pass fromBlock
const launches = streamLaunches(hood, { fromBlock: 7_000_000n })
launches.on('data', (l) => console.log(l.launchpad, l.token, l.creator))
Overflow policy controls the async-iterator buffer when a consumer falls behind:
'drop-oldest' (default for swaps/launches — keep the newest, count the rest in
stream.dropped), 'latest' (default for prices — only the current
value matters), or 'block' (never drop; only for naturally slow producers).
cache — read-through + request coalescing
N concurrent identical reads collapse into exactly 1 upstream call. Per-datatype TTLs match how fast each thing actually changes: quotes 2s, portfolios 5s, multipliers 10min, registry 1h.
import { createHoodClient } from 'hoodchain'
import { createHoodCache, MemoryLruStore } from 'hoodkit'
const hood = createHoodClient()
const cache = createHoodCache(hood, {
ttls: { quote: 2_000 }, // override any subset of the defaults
})
// 100 concurrent calls -> 1 RPC round-trip
const results = await Promise.all(Array.from({ length: 100 }, () => cache.getQuote('AAPL')))
console.log(cache.stats) // { hits, misses: 1, coalesced: 99 }
// Bring your own store — e.g. Redis, so the cache survives process restarts
const store = {
async get(key) { const v = await redis.get(key); return v ? JSON.parse(v) : undefined },
async set(key, value, ttlMs) { await redis.set(key, JSON.stringify(value), 'PX', ttlMs) },
async delete(key) { await redis.del(key) },
}
const redisCache = createHoodCache(hood, { store })
batch — multicall aggregation
plan() batches arbitrary reads into the fewest Multicall3 round-trips (chunked,
each read isolated — one revert can't sink the batch). createBatcher() coalesces
reads from unrelated call sites within the same tick, DataLoader-style.
import { createHoodClient, erc20Abi, listStockTokens } from 'hoodchain'
import { plan, createBatcher } from 'hoodkit'
const hood = createHoodClient()
// One-shot: read every Stock Token's totalSupply in a handful of multicalls
const results = await plan(hood, listStockTokens().map((t) => ({
address: t.address, abi: erc20Abi, functionName: 'totalSupply',
})))
// Ongoing: independent code paths sharing one multicall automatically
const batcher = createBatcher(hood)
const [a, b, c] = await Promise.all([
batcher.call({ address: t1, abi: erc20Abi, functionName: 'balanceOf', args: [me] }),
batcher.call({ address: t2, abi: erc20Abi, functionName: 'balanceOf', args: [me] }),
batcher.call({ address: t3, abi: erc20Abi, functionName: 'balanceOf', args: [me] }),
]) // one multicall, not three
indexer — local SQLite index
Syncs Transfer and Uniswap v3 Swap events for a token set into
SQLite, resuming from the last synced block. Requires the optional
better-sqlite3 peer dependency.
import { createHoodClient } from 'hoodchain'
import { createIndexer } from 'hoodkit'
const hood = createHoodClient()
const indexer = await createIndexer({
client: hood,
path: './hood.sqlite',
tokens: ['0x…WEN'],
chunkSize: 50_000n,
throttleMs: 100, // be polite to the public RPC on a full backfill
})
// Full history is needed for an accurate absolute holder count; swap history
// can be scoped separately (candles rarely need the whole multi-day log).
const head = await hood.public.getBlockNumber()
await indexer.sync({ fromBlock: 0n, swapFromBlock: head - 50_000n })
console.log(indexer.holderCount('0x…WEN')) // verified against Blockscout in CI
console.log(indexer.candles('0x…WEN', '1h')) // real OHLCV from indexed swaps
console.log(indexer.volume24h('0x…WEN')) // trailing 24h token volume
Balances are recomputed exactly in JS bigints from raw transfer deltas — SQLite's
SUM() silently loses precision past ~9.2e18, which 18-decimal token values
routinely exceed.
strategy — agent primitives
Position PnL, TWAP execution, price triggers, and a hard spend cap — the primitives an autonomous trading agent needs, with a dry-run mode on by default.
import { createHoodClient, MAINNET_ADDRESSES, parseUsdg } from 'hoodchain'
import { Position, SpendCap, createTwapExecutor, createPriceTriggers } from 'hoodkit'
// Multiplier-aware PnL
const position = new Position()
position.record({ side: 'buy', quantity: 10, price: 100 })
position.record({ side: 'sell', quantity: 4, price: 150 })
console.log(position.snapshot(120)) // { realized, unrealized, total, shareEquivalent, ... }
// TWAP with a hard spend cap and a kill switch
const hood = createHoodClient()
const controller = new AbortController()
const twap = createTwapExecutor(hood, {
tokenIn: MAINNET_ADDRESSES.usdg,
tokenOut: MAINNET_ADDRESSES.weth,
totalAmountIn: parseUsdg('1000'),
slices: 5,
spendCap: new SpendCap(parseUsdg('1000')),
signal: controller.signal,
})
const results = await twap.run() // dry-run (simulated) unless the client has a wallet
// Price triggers
const triggers = createPriceTriggers(hood)
triggers.onCross('AAPL', 250, 'up', (e) => console.log('AAPL broke $250:', e.price))
hoodkit/react — SSR-safe hooks
All subscriptions live inside useEffect, so components render inert on the server
and hydrate on the client.
import { HoodProvider, useQuote, usePortfolio, useLaunches, useSwap } from 'hoodkit/react'
import { createHoodClient } from 'hoodchain'
function App() {
const client = createHoodClient()
return (
<HoodProvider client={client}>
<Dashboard />
</HoodProvider>
)
}
function Dashboard() {
const { data: quote } = useQuote('AAPL')
const { data: portfolio } = usePortfolio('0xYourAddress')
const { launches } = useLaunches({ limit: 20 })
const { getQuote, swap, isSwapping } = useSwap()
return (
<div>
<p>AAPL: {quote ? `$${quote.priceUsd}` : '…'}</p>
<p>Portfolio: {portfolio ? `$${portfolio.totalUsd.toFixed(2)}` : '…'}</p>
<ul>{launches.map((l) => <li key={l.transactionHash}>{l.launchpad}: {l.token}</li>)}</ul>
</div>
)
}
See the fully working demo in examples/react-demo/ (run with npm run dev).