Guides

One section per module, each with a real, copy-pasteable example against live chain data.

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).