Developers

Subgraph

On-chain reads cover current state; the subgraph serves everything historical and derived — real compounded APY, share-price charts, TVL over time, and per-user P&L and cost basis.

Endpoint#

GraphQL over HTTP POST. Use the prod tag — it always serves the latest deploy, so the URL never changes:

text
https://api.goldsky.com/api/public/project_cmgzljqwl006c5np2gnao4li4/subgraphs/topaz-ovault/prod/gn

Minimal client#

subgraph.ts
const SUBGRAPH_URL =
  "https://api.goldsky.com/api/public/project_cmgzljqwl006c5np2gnao4li4/subgraphs/topaz-ovault/prod/gn";

export async function query<T>(q: string, variables?: Record<string, unknown>): Promise<T> {
  const res = await fetch(SUBGRAPH_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query: q, variables }),
  });
  const json = await res.json();
  if (json.errors) throw new Error(JSON.stringify(json.errors));
  return json.data;
}

Works with Apollo / urql / graphql-request too — it's a standard subgraph.

Conventions you must know#

  • All numbers are strings. Parse with a bignumber lib; never do BigInt math in JS number.
  • USD fields are already human dollars (tvlUSD, sharePriceUSD, *PnlUSD) — render directly.
  • Token amount fields are already decimal-adjusted (underlyingAmount0/1, amount0/1) — do not divide by 10^decimals.
  • Shares are raw — humanize by 10^vault.decimals (CL = 18, V2 = 24). Always read vault.decimals.
  • Token USD price = Token.derivedETH × Bundle.ethPriceUSD (Bundle id "1").
  • Addresses are lowercase in ids / Bytes filters.
  • Attribution is handled: zapper-routed deposits credit the real wallet. Query Account/Position; ignore intermediaries.

Mark positions live, not stale

Position.currentValueUSD / unrealizedPnlUSD / totalPnlUSDare marked at the user's last interaction. For a live figure, recompute from the always-current Vault.sharePriceUSD (recipe below).

Key entities#

EntityWhat it holds
VaultOne per vault: type (CL/V2), tokens, tvlUSD, sharePriceUSD, apy/rewardApr, tick/range/inRange (CL), fees, cumulative flows, activity counts.
PositionOne per (vault, wallet): shares, underlying amounts, depositedUSD/withdrawnUSD, cost basis, realized/unrealized/total P&L, vs-HODL inputs.
InteractionImmutable per-tx ledger: DEPOSIT | WITHDRAW | ZAP_IN | ZAP_OUT | TRANSFER_*, with shares, amount0/1, deltaUSD, sharePriceUSD, entryFeeUSD, via, txHash.
VaultSnapshotTime-series buckets (period 3600 hourly / 86400 daily): tvlUSD, sharePrice OHLC, flows, rewardApr, inRange, tick.
Harvest / Rebalance / ConfigChangeImmutable event logs: compounded rewards + fees + aprSlice; old/new ticks; config field changes.
Token / Pool / Bundle / ProtocolPricing & protocol: derivedETH, ethPriceUSD anchor, pool prices, protocol-wide TVL and controls.

CL-only fields (tick, tickLower/Upper, inRange, positionWidth…) are null/0 for V2 — branch on Vault.type.

Query cookbook#

Vault list#

graphql
{
  vaults(orderBy: tvlUSD, orderDirection: desc) {
    id name symbol type
    tvlUSD sharePriceUSD apy rewardApr
    inRange paused positionCount
    token0 { symbol decimals } token1 { symbol decimals } base { symbol }
    underlyingAmount0 underlyingAmount1
    harvestCount lastHarvestTimestamp
  }
}

Vault charts (TVL / share-price / APR over time)#

graphql
query VaultChart($vault: Bytes!, $period: Int!, $since: BigInt!) {
  vaultSnapshots(
    where: { vault: $vault, period: $period, periodStartUnix_gte: $since }
    orderBy: periodStartUnix orderDirection: asc
    first: 1000
  ) {
    periodStartUnix
    tvlUSD sharePriceUSD
    sharePriceOpen sharePriceHigh sharePriceLow sharePriceClose
    depositUSD withdrawUSD harvestUSD rewardApr
    inRange tick
  }
}

period: 3600 hourly, 86400 daily. since: unix seconds lower bound.

User dashboard (all of a wallet's positions)#

graphql
query Positions($account: Bytes!) {
  positions(where: { account: $account, shares_gt: "0" }) {
    id shares
    currentValueUSD depositedUSD withdrawnUSD costBasisRemainingUSD
    realizedPnlUSD unrealizedPnlUSD totalPnlUSD avgEntryPriceUSD
    initialDeposit0 initialDeposit1 hodlCostUSD
    firstInteractionTimestamp lastInteractionTimestamp
    vault {
      id name symbol decimals sharePriceUSD inRange
      token0 { symbol decimals derivedETH } token1 { symbol decimals derivedETH }
    }
  }
  bundle(id: "1") { ethPriceUSD }
}

Position detail + transaction history#

graphql
query PositionHistory($id: ID!) {
  position(id: $id) {
    shares currentValueUSD depositedUSD withdrawnUSD
    realizedPnlUSD unrealizedPnlUSD totalPnlUSD avgEntryPriceUSD
    vault { symbol decimals sharePriceUSD token0 { symbol derivedETH } token1 { symbol derivedETH } }
  }
  interactions(where: { position: $id } orderBy: timestamp orderDirection: desc first: 100) {
    type shares amount0 amount1 deltaUSD sharePriceUSD entryFeeUSD via priceSuspect timestamp txHash
  }
}

$id = {vaultAddress}-{walletAddress}, both lowercased.

Live value & P&L recipe#

ts
// Recompute live from the always-current Vault.sharePriceUSD
const shareUnits   = Number(pos.shares) / 10 ** Number(vault.decimals); // CL 1e18, V2 1e24
const liveValueUSD = shareUnits * Number(vault.sharePriceUSD);
const totalPnlUSD  = liveValueUSD + Number(pos.withdrawnUSD) - Number(pos.depositedUSD);
const pnlPct       = (totalPnlUSD / Number(pos.depositedUSD)) * 100;

// vs-HODL: did the vault beat holding the two deposited tokens?
const usd = (t, ethUSD) => Number(t.derivedETH) * Number(ethUSD);
const hodlNowUSD =
  Number(pos.initialDeposit0) * usd(vault.token0, ethUSD) +
  Number(pos.initialDeposit1) * usd(vault.token1, ethUSD);
const vsHodlUSD = liveValueUSD - hodlNowUSD;

Gotchas#

  • Pagination: default 100, max 1000. Keyset-paginate large sets on a sortable field.
  • Σ positions ≠ totalSupply by exactly 1000 wei per vault (dead shares). Use Vault.tvlUSD for vault TVL.
  • apy / aprSlice is 0 until the second harvest. Fall back to a trailing average if you want a smoother number.
  • priceRangeMin/Max are 0 — compute range price from ticks.
  • Treat priceSuspect: true rows as “USD unavailable/approximate.”
  • Poll { _meta { block { number } hasIndexingErrors } } for a “data as of block N” indicator.

Keep reading#

Data APISmart contractsHow yield works