getSolverRevenue
Read what the solver has earned on one market — total, split into the hedger-fee share and the funding share — over a trailing window. symbolId names the market and is required; the window defaults to lifetime.
import { getSolverRevenue } from "@symmio/trading-core";
const revenue = await getSolverRevenue(config, { symbolId: 1 });
revenue.totalRevenue; // 15936.3 — dollars, market 1, since listing
revenue.hedgerFeeRevenue; // the share earned from hedger fees
revenue.fundingRevenue; // the share earned from funding payments
revenue.recordCount; // how many underlying rows produced those totalsThis is the solver’s own bookkeeping, not the inventory service’s and not the listing backend’s. A pools dashboard that shows revenue next to TVL next to a catalog row is reading three different vendors — see Pools for how they divide up.
The two halves of the signature are exported as GetSolverRevenueParameters (the parameters object) and GetSolverRevenueReturnType (an alias of SolverRevenue), so you can name them in your own wrappers:
import type { GetSolverRevenueParameters, GetSolverRevenueReturnType } from "@symmio/trading-core";
function lastDay(parameters: GetSolverRevenueParameters): Promise<GetSolverRevenueReturnType> {
return getSolverRevenue(config, { ...parameters, timeRange: "24h" });
}The read is per-market
There is no protocol-wide variant. Earlier solver generations served an aggregate /revenue endpoint and this
action exposed it by omitting symbolId; the current generation removed that endpoint, so symbolId is now required
and the read answers for exactly one market.
For a figure that spans several markets, call once per market and label the result with the markets it covers — do not sum a handful of markets and call it “protocol revenue”. A sum over markets [1, 2, 3] is the revenue of markets 1–3, nothing more, and every market you skip makes the label more wrong.
The underlying rows are still available: getRevenueRecords pages through every revenue-bearing row (optionally filtered by symbol), which is the honest source for anything cross-market.
Parameters
chainIdnumberoptionalTarget chain id. Defaults to the config’s defaultChainId. Selects which chain’s solver is read, and is folded into
the query key.
Solver kind to target. Defaults to the chain’s default solver. Must resolve to an enigma solver — see
Enigma-only.
symbolIdnumberThe market whose revenue to read. Required — the read hits /revenue/{symbolId} and there is no aggregate endpoint
to fall back to.
timeRangeSolverRevenueTimeRangedefault "lifetime"optionalTrailing window: "1h", "24h", "7d", "30d" or "lifetime". Omitted entirely from the request when unset, so
the solver applies its own default of lifetime.
timeRange is a closed set
type SolverRevenueTimeRange = "1h" | "24h" | "7d" | "30d" | "lifetime";Those five literals are the whole vocabulary. The solver validates the value server-side and rejects anything else with TimeRange must be one of [1h 24h 7d 30d lifetime], so an arbitrary duration string, a number of seconds, or a date range is a failed request rather than a best-effort answer.
Windows are trailing from now, not calendar buckets: "24h" is the last twenty-four hours, not “today”. Each window is an independent request — there is no series endpoint here, so a sparkline means one call per point (or getRevenueRecords for the underlying rows).
Returns
Promise<SolverRevenue>totalRevenuenumberTotal revenue over the window, in dollars — 15936.3 means $15,936.30. Equal to hedgerFeeRevenue +
fundingRevenue.
hedgerFeeRevenuenumberThe share earned from hedger fees, in dollars.
fundingRevenuenumberThe share earned from funding payments, in dollars.
recordCountnumberHow many underlying revenue rows the totals were computed from. Surface it: it is what separates “this window
genuinely earned nothing” (recordCount > 0, totals 0) from “there is no data for this window”
(recordCount === 0).
No decimal scaling. Unlike the on-chain reads and the listing catalog’s 18-decimal bigints, these are plain
JavaScript numbers already in dollar units. Do not run them through formatUnits; format them as currency directly.
Every field is optional in the solver’s own schema — it omits a dimension rather than sending a zero for it — so each one is defaulted to 0 on the way through. A response with nothing in it is { totalRevenue: 0, hedgerFeeRevenue: 0, fundingRevenue: 0, recordCount: 0 }, never undefined fields.
Because totalRevenue is the sum of the other two, the split is a share breakdown you can chart without a second call:
const { totalRevenue, hedgerFeeRevenue, fundingRevenue } = await getSolverRevenue(config, { symbolId: 1 });
const feeShare = totalRevenue === 0 ? 0 : (hedgerFeeRevenue / totalRevenue) * 100;
const fundingShare = totalRevenue === 0 ? 0 : (fundingRevenue / totalRevenue) * 100;Enigma-only
The /revenue/{symbolId} endpoint exists on the enigma solver only. getSolverRevenue resolves the target solver and checks its kind before the network hop, so a rasa target fails immediately with UNSUPPORTED_BY_SOLVER instead of surfacing a vendor 404 that reads like an outage.
import { getSolverRevenue } from "@symmio/trading-core";
import { base } from "viem/chains";
// Throws UNSUPPORTED_BY_SOLVER — Base's default solver is rasa-kind. No request is made.
await getSolverRevenue(config, { chainId: base.id, symbolId: 1 });Reaching that check means resolving a rasa solver, which is why the example switches chains rather than forcing solverId: "rasa". Asking for a solver kind a chain does not register — "rasa" on a chain that carries the enigma solver alone — fails one step earlier with UNKNOWN_SOLVER.
In a UI that can point at either solver, gate the panel on the resolved kind rather than catching the error.
Examples
One market, since listing
No timeRange — the solver’s own default window is lifetime:
const lifetime = await getSolverRevenue(config, { symbolId: 1 });
lifetime.totalRevenue; // 15936.3
lifetime.recordCount; // > 0, so those totals are real and not a data gapThe trailing 24 hours
const day = await getSolverRevenue(config, { symbolId: 1, timeRange: "24h" });
if (day.recordCount === 0) {
// No rows in the window at all — render "no data", not "$0.00".
}Several markets, labeled honestly
One call per market, and the label names what the sum covers:
const symbolIds = [1, 2, 3];
const totals = await Promise.all(symbolIds.map((symbolId) => getSolverRevenue(config, { symbolId, timeRange: "30d" })));
const markets123 = totals.reduce((sum, t) => sum + t.totalRevenue, 0);
// Render as "Revenue · markets 1–3", not "Protocol revenue".Query options
import { getSolverRevenueQueryOptions } from "@symmio/trading-core";
import { useQuery } from "@tanstack/react-query";
useQuery(getSolverRevenueQueryOptions(config, { symbolId: 1, timeRange: "24h" }));GetSolverRevenueOptions is the action’s parameters plus a query bag of TanStack overrides. The factory folds config.getChainConfigKey(chainId) into the key, so a runtime config override pointed at a different solver deployment refetches instead of serving the previous one’s cache. getSolverRevenueQueryKey builds the same key for cache matching and invalidation.
Both symbolId and timeRange are part of the key, so each market/window pair caches independently — one market’s 24-hour figure never gets overwritten by another market’s fetch.
The rest of the factory’s types are exported too: GetSolverRevenueData is what the query resolves to (the same SolverRevenue), GetSolverRevenueQueryKey is the key getSolverRevenueQueryKey returns, and GetSolverRevenueQueryOptions is the options bag the factory produces.
Throws
UNSUPPORTED_CHAIN— aSymmError(kind: "config") when thechainIdis not one the config knows about at all.UNKNOWN_SOLVER— aSymmError(kind: "config") when the resolved chain registers no solver of the requested kind. Raised before the kind check, so it precedesUNSUPPORTED_BY_SOLVER.UNSUPPORTED_BY_SOLVER— aSymmError(kind: "config") when the resolved solver is not enigma-kind. Raised before any request.FETCH_SOLVER_REVENUE_FAILED— the request itself failed. Any axios failure becomes aSymmApiErrorcarryingstatus,statusText,responseData,urlandmethod; a non-axios throw becomes a plainSymmError(kind: "api"), with the original error as itscausewhen it was anError.
A malformed timeRange surfaces through the second code: the solver answers with its TimeRange must be one of [1h 24h 7d 30d lifetime] message, which arrives as the SymmApiError’s responseData.
Related
- Solvers — the slice overview, including
getRevenueRecordsfor the individual rows behind these totals. - Solvers hooks —
useSolverRevenueis the React wrapper over this action. - Solvers & Chains — the solver-kind model this read’s enigma-only constraint belongs to.
- Errors — the
SymmError/SymmApiErrorhierarchy these codes belong to.