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:
https://api.goldsky.com/api/public/project_cmgzljqwl006c5np2gnao4li4/subgraphs/topaz-ovault/prod/gnMinimal client#
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 readvault.decimals. - Token USD price =
Token.derivedETH × Bundle.ethPriceUSD(Bundle id"1"). - Addresses are lowercase in ids /
Bytesfilters. - 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#
| Entity | What it holds |
|---|---|
Vault | One per vault: type (CL/V2), tokens, tvlUSD, sharePriceUSD, apy/rewardApr, tick/range/inRange (CL), fees, cumulative flows, activity counts. |
Position | One per (vault, wallet): shares, underlying amounts, depositedUSD/withdrawnUSD, cost basis, realized/unrealized/total P&L, vs-HODL inputs. |
Interaction | Immutable per-tx ledger: DEPOSIT | WITHDRAW | ZAP_IN | ZAP_OUT | TRANSFER_*, with shares, amount0/1, deltaUSD, sharePriceUSD, entryFeeUSD, via, txHash. |
VaultSnapshot | Time-series buckets (period 3600 hourly / 86400 daily): tvlUSD, sharePrice OHLC, flows, rewardApr, inRange, tick. |
Harvest / Rebalance / ConfigChange | Immutable event logs: compounded rewards + fees + aprSlice; old/new ticks; config field changes. |
Token / Pool / Bundle / Protocol | Pricing & 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#
{
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)#
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)#
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#
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#
// 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 ≠ totalSupplyby exactly 1000 wei per vault (dead shares). UseVault.tvlUSDfor vault TVL.apy/aprSliceis 0 until the second harvest. Fall back to a trailing average if you want a smoother number.priceRangeMin/Maxare 0 — compute range price from ticks.- Treat
priceSuspect: truerows as “USD unavailable/approximate.” - Poll
{ _meta { block { number } hasIndexingErrors } }for a “data as of block N” indicator.