Statistics
Public volume, open interest, all-time totals, and the trader leaderboard, scoped to your client ID.
Four endpoints publish Pear's traded volume, open interest, all-time totals, and trader leaderboard. Pass your client ID on any of them and every figure narrows to what your integration routed.
| Endpoint | SDK method | Returns |
|---|---|---|
GET /statistics/volume | sdk.core.statistics.volume() | Volume, fees, trades, users, and new users, per time bucket. |
GET /statistics/oi | sdk.core.statistics.oi() | Open interest and open position count, per time bucket. |
GET /statistics/totals | sdk.core.statistics.totals() | All-time volume, fees, trades, and users. |
GET /statistics/leaderboard | sdk.core.statistics.leaderboard() | Trade accounts ranked by net PnL, volume, trade count, or ROI. |
All four are public
These endpoints need no credential at all. No x-api-key, no bearer token, no session cookie, and no x-trade-account-id. They sit with the market-data endpoints on the open side of the gateway, not with the account-scoped ones that fill the rest of this section.
curl "https://pro-gateway.pearprotocol.io/statistics/totals?connector=hyperliquid"Two things follow from that.
- You can call them from anywhere, including a browser, a public status page, or a dashboard that holds no secret.
clientIdis a filter, not a permission. Anyone may pass any code and read that integration's slice. Do not treat your own numbers as private, and do not treat the code as a secret. See Client ID.
Sending a key anyway is harmless. It changes nothing in the response.
Volume and open interest
Both are time series over fixed buckets. They share the same four parameters.
| Parameter | Required | Rule |
|---|---|---|
connector | Yes | One of hyperliquid, binance, bybit, okx, lighter. One venue per call. |
resolution | Yes | 1h, 4h, or 24h. Selects the bucket width. |
startDate | No | ISO 8601 date-time. Inclusive. Absent opens the range at the epoch. |
endDate | No | ISO 8601 date-time. Inclusive. Defaults to now. |
clientId | No | Your code. Absent serves the protocol-wide series. Trimmed before it is matched. |
startDate must be strictly before endDate, or the call fails with 400 and the message startDate must be before endDate. Because endDate defaults to now, a startDate in the future fails on its own.
Both bounds are compared against the bucket's start time, and both are inclusive.
Dates here are ISO 8601 strings, not Unix seconds. startDate=2026-08-01 and startDate=2026-08-01T00:00:00Z both work; startDate=1785542400 is rejected with 400. The leaderboard is the opposite way round, so the two date formats are not interchangeable. See Two date formats.
There is no pagination. Narrow the window instead.
Volume
curl "https://pro-gateway.pearprotocol.io/statistics/volume?connector=hyperliquid&resolution=24h&startDate=2026-08-24&clientId=your-client-id"{
"data": [
{ "timestamp": 1787529600000, "volume": 4820134.55, "fees": 2892.08, "trades": 314, "users": 87, "newUsers": 12 },
{ "timestamp": 1787616000000, "volume": 5103998.2, "fees": 3062.4, "trades": 341, "users": 92, "newUsers": 9 },
{ "timestamp": 1787702400000, "volume": 1877420.06, "fees": 1126.45, "trades": 128, "users": 44, "newUsers": 3 }
]
}| Field | Meaning |
|---|---|
timestamp | The bucket's start, in epoch milliseconds. |
volume | Filled notional in USD. |
fees | Fees charged on those fills, in USD. |
trades | Fills in the bucket. |
users | Distinct users who filled in the bucket. |
newUsers | Users whose first ever fill on this scope landed in this bucket. |
Every field is a JSON number, not a decimal string. These are aggregate counters, where float rounding does not show.
Rows arrive oldest first, and only buckets that hold activity are returned. A quiet day is a missing row, not a zero row, so fill gaps yourself when you chart the series.
What newUsers counts
newUsers is new to you, not new to Pear. On a client-scoped call it counts a user whose first ever fill through your client ID on that connector landed in the bucket. Nothing about it asks when the user joined Pear.
Three consequences:
- A trader who has used Pear for a year, through the web app or another integration, still counts as new the first time they fill through you.
- The same trader counts as new again on a second venue. The first fill is tracked per connector, so trading through you on Hyperliquid and later on Binance is two new users, one in each connector's series.
- Once counted, never counted again. The first fill is recorded once, so
newUsersnever repeats a user on the same connector and client.
Omit clientId and newUsers reverts to its protocol meaning: the user's first ever fill on that connector, whoever routed it.
users is a different question. It counts distinct users active in the bucket, so the same trader appears in every bucket they trade in.
Open interest
curl "https://pro-gateway.pearprotocol.io/statistics/oi?connector=hyperliquid&resolution=24h&startDate=2026-08-24&clientId=your-client-id"{
"data": [
{ "timestamp": 1787529600000, "openInterest": 5214880.12, "openPositions": 63 },
{ "timestamp": 1787616000000, "openInterest": 5680142.9, "openPositions": 71 },
{ "timestamp": 1787702400000, "openInterest": 5402011.37, "openPositions": 68 }
]
}| Field | Meaning |
|---|---|
timestamp | The bucket's start, in epoch milliseconds. |
openInterest | Mark-valued notional of the open positions, in USD. |
openPositions | How many positions were open. |
Open interest is a snapshot, not a sum. Pear takes it once an hour, on the hour, in UTC. A bucket carries the latest snapshot taken inside it, so a 24h row is one moment's open interest, not the day's average or its peak. Raising resolution keeps fewer of the same snapshots; it never aggregates them.
A client-scoped row values the positions whose opening fill carried your code. A position you opened and another integration later adjusted still counts as yours in full.
All-time totals
curl "https://pro-gateway.pearprotocol.io/statistics/totals?connector=hyperliquid&clientId=your-client-id"{
"data": {
"totalVolume": 918442310.77,
"totalFees": 551065.38,
"totalTrades": 61204,
"totalUsers": 3182
}
}| Parameter | Required | Rule |
|---|---|---|
connector | Yes | One venue per call. |
clientId | No | Absent serves the protocol-wide totals. Trimmed before it is matched. |
Totals take no resolution and no date range. They are all-time by definition, and there is no way to bound them. Use the volume series when you need a window.
totalUsers counts distinct users who ever filled on that connector in this scope. On a client-scoped call that is the lifetime count of traders you brought to that venue, the same population newUsers accumulates.
Leaderboard
The leaderboard ranks trade accounts, not integrations and not users. One trader with accounts on two venues earns two rows, joinable on userId.
curl "https://pro-gateway.pearprotocol.io/statistics/leaderboard?sortBy=totalNetPnl&limit=25&clientId=your-client-id"{
"data": [
{
"tradeAccountId": "6f1c0a5e-9d43-4a71-8b2e-1c7f5a90d312",
"userId": "b3e91d27-0f4a-4c8d-9a15-72d6e4b81f09",
"connector": "hyperliquid",
"volume": "1284500.42",
"grossRealizedPnl": "18420.11",
"grossUpnl": "2310.75",
"fundingPayment": "-412.6",
"totalNetPnl": "19507.94",
"pearFee": "449.58",
"tradeFee": "360.74",
"totalTrades": 214,
"roi": "0.1832",
"xHandle": "pairtrader",
"xName": "Pair Trader",
"xAvatarUrl": "https://pbs.twimg.com/profile_images/1450031234567890123/abcdef_400x400.jpg",
"xUserId": "1450031234567890123"
}
]
}Parameters
| Parameter | Default | Rule |
|---|---|---|
sortBy | totalNetPnl | totalNetPnl, volume, trades, or roi. |
limit | 200 | 1 to 200. Above 200 is rejected with 400, never clamped. |
connector | every venue | Repeat the parameter or comma-separate it: connector=hyperliquid&connector=lighter. |
asset | every asset | Same repeat-or-comma form. Trimmed, and matched case-insensitively. |
startDate | unbounded | Unix seconds, a non-negative integer. Inclusive. |
endDate | unbounded | Unix seconds, a non-negative integer. Exclusive. |
includeUnlinkedX | false | true or false. See below. |
clientId | every client | Your code. Not trimmed. |
startDate must be strictly before endDate where both are given, or the call fails with 400.
There is no cursor and no offset. limit is the only size control, and the board never returns more than 200 rows. There is no order parameter either: ranking is descending, always. Ties break on tradeAccountId ascending, so the boundary of the cut is stable between requests.
connector and asset filter before the top-N cut. Two connectors give you the top N across both combined, not N of each.
The asset filter works per leg. A long-BTC / short-ETH basket contributes only its BTC leg to an asset=BTC board.
Two date formats
The leaderboard reads startDate and endDate as Unix seconds. Volume and open interest read them as ISO 8601 strings. Neither accepts the other's format.
| Endpoint | Format | Example |
|---|---|---|
/statistics/volume, /statistics/oi | ISO 8601 date-time | startDate=2026-08-01T00:00:00Z |
/statistics/leaderboard | Unix seconds | startDate=1785542400 |
Milliseconds are wrong on both. On the leaderboard a millisecond value is read as seconds and lands tens of thousands of years out; on volume and open interest it is rejected with 400.
What the board counts
Only positions the Pear engine created are ranked. A position imported from the venue never reaches the board, whatever its size. See Synthetic Position.
Within those positions, only fills the engine placed count. Fills synced from the exchange, reconstruction artefacts, and superseded rows are all excluded.
Traded activity and earned money are scoped differently by the date window. This is the part that surprises people:
| Group | Rule |
|---|---|
volume, totalTrades | Every counted fill inside the window, whatever period its position opened in. |
grossRealizedPnl, grossUpnl, pearFee, tradeFee, roi | Only positions whose opening fill lands at or after startDate. |
So volume is additive across two adjacent windows and PnL is not. A row can also report real volume beside zero fees and zero PnL, when the only trading in the window belonged to a position that opened before it.
grossUpnl is included whenever the window still contains now, which means an absent endDate or one that has not yet arrived. A window that has already closed returns "0" for it, because unrealized PnL is a value as of now and now sits outside such a window.
totalNetPnl is grossRealizedPnl minus tradeFee and pearFee, plus fundingPayment, plus grossUpnl.
fundingPayment is signed and negative when the trader paid to hold. It is measured per trade account, not per client, so a client-scoped board reports the account's whole funding carry, including carry on positions another integration opened. It is the one figure on a scoped row that is not a subset of your own activity.
roi is the plain average of the account's per-position returns, each measured against the size that position opened with. "0.1832" is +18.32%. Every position weighs the same, so one small winner counts as much as a career of large ones, and there is no minimum size or trade count. It is omitted, never "0", when the board could measure none of the account's positions. Under sortBy=roi, a row with no roi ranks below every row that has one, whatever the sign.
The X gate hides most of your traders by default
includeUnlinkedX defaults to false, and the gate applies before the top-N cut. An account whose owner linked no X account does not take a slot and then get dropped; it never enters the ranking at all.
A partner reading their own board therefore sees only the traders who linked X, and the board silently looks smaller than the integration is. If you want your real top traders, pass includeUnlinkedX=true.
curl "https://pro-gateway.pearprotocol.io/statistics/leaderboard?clientId=your-client-id&includeUnlinkedX=true&limit=50"A trader who linked X and then hid the handle keeps their row and loses only their name: the row comes back with no xHandle, xName, xAvatarUrl, or xUserId.
The X fields are the same for every caller. Signing in reveals nothing extra, and the only person who can withhold a handle is its owner.
The row
| Field | Type | Meaning |
|---|---|---|
tradeAccountId | string | The row's identity. Render or join by this. |
userId | string | The trader behind the account, so you can group their rows across venues. |
connector | string | The account's venue. |
volume | decimal string | Filled notional in the window. |
grossRealizedPnl | decimal string | Realized PnL before fees. |
grossUpnl | decimal string | Unrealized PnL, marked now. "0" once the window has closed. |
fundingPayment | decimal string | Net funding carry, signed. Account-level. |
totalNetPnl | decimal string | Realized, less both fees, plus funding, plus unrealized. |
pearFee | decimal string | Pear's fee on the counted fills. |
tradeFee | decimal string | The venue's fee on the counted fills. |
totalTrades | number | Counted fills in the window. |
roi | decimal string, optional | Average per-position return, as a ratio. Absent when unmeasurable. |
xHandle | string, optional | Linked X handle. Absent if unlinked or hidden. |
xName | string, optional | Linked X display name. |
xAvatarUrl | string, optional | Linked X avatar. |
xUserId | string, optional | Immutable X numeric id. |
Money is a decimal string on this endpoint, because these are per-account figures where float rounding would show. The volume, open interest, and totals endpoints use JSON numbers instead, because those are aggregate counters.
xAvatarUrl points at X's own image host, rewritten to the 400x400 variant where X's naming allows it.
Join on xUserId, never on xHandle. X frees a renamed handle for anyone to claim, so a cached handle can point at a stranger. The handle Pear holds is whatever X reported at the owner's last authorization, so a trader who renamed and has not signed in since is named by their old handle.
The board publishes no wallet address and no exchange identifier, by design.
Scope every figure to your client ID
clientId behaves the same way on all four endpoints in the ordinary case: pass your code and you get your own slice; omit it and you get the protocol-wide figures.
Attribution follows the position's opening fill, so a position belongs to exactly one integration and your numbers are always a subset of the protocol's, never larger. See Where the code is recorded.
Three behaviours differ between the endpoints, and all three bite in practice.
clientId=other returns nothing on the leaderboard
other selects the unattributed bucket: every trade that carried no code at all.
| Endpoint | clientId=other |
|---|---|
/statistics/volume | The unattributed series. Matched case-insensitively. |
/statistics/oi | The unattributed series. Matched case-insensitively. |
/statistics/totals | The unattributed totals. Matched case-insensitively. |
/statistics/leaderboard | { "data": [] }. |
The leaderboard has no unattributed branch. It looks other up as a real code, finds none, and returns an empty array. Do not read that as "nobody traded unattributed".
The leaderboard does not trim clientId
clientId is trimmed on volume, open interest, and totals. It is not trimmed on the leaderboard.
A trailing space is therefore forgiven on three endpoints and fatal on the fourth, where it returns an empty board rather than an error. Send the exact string everywhere.
Matching is exact and case-sensitive on all four. Pear-Web is not pear-web.
An empty or whitespace-only clientId also splits: volume, open interest, and totals reject it with 400, while the leaderboard accepts it and serves the protocol-wide board.
An unknown code is not an error
A code Pear does not recognize returns an empty result, not a failure.
| Endpoint | Unknown code |
|---|---|
/statistics/volume | { "data": [] } |
/statistics/oi | { "data": [] } |
/statistics/totals | { "data": { "totalVolume": 0, "totalFees": 0, "totalTrades": 0, "totalUsers": 0 } } |
/statistics/leaderboard | { "data": [] } |
This is the opposite of the trading endpoints, where an unknown or inactive clientId fails the call with 400. An empty statistics response is therefore ambiguous: it means either "no activity" or "you typed the code wrong". Verify the code once against a trading call, or against a window you know has volume, before you trust an empty chart.
Deactivation does not apply here either. A code Pear has deactivated still returns its historical figures, even though trading calls that carry it now fail.
With the SDK
const totals = await sdk.core.statistics.totals({
connector: 'hyperliquid',
clientId: 'your-client-id',
});
const series = await sdk.core.statistics.volume({
connector: 'hyperliquid',
resolution: '24h',
startDate: new Date('2026-08-01T00:00:00Z'),
clientId: 'your-client-id',
});
const board = await sdk.core.statistics.leaderboard({
clientId: 'your-client-id',
sortBy: 'volume',
includeUnlinkedX: true,
connector: ['hyperliquid', 'lighter'],
limit: 50,
});leaderboard() is the only one whose argument is optional; the other three need at least connector. Array parameters are sent as repeated query parameters.
The SDK's configured clientId is not applied here. It fills in the body of trading calls only, so pass clientId in the query object yourself on every statistics call.
These calls need no authenticated SDK instance and no trade account, but an authenticated one works just as well.
What gets rejected
Every failure below is a 400 with the standard validation envelope: message is "Validation error" and details names the field. See Error Handling.
| Cause | Endpoints |
|---|---|
connector missing | volume, OI, totals |
connector not one of the five venues | all four |
resolution missing, or not 1h, 4h, or 24h | volume, OI |
startDate at or after endDate | volume, OI, leaderboard |
startDate or endDate not an ISO 8601 date-time | volume, OI |
startDate or endDate negative, fractional, or not a number | leaderboard |
clientId empty or whitespace only | volume, OI, totals |
limit above 200, zero, negative, or fractional | leaderboard |
sortBy not one of the four metrics | leaderboard |
asset present but whitespace only | leaderboard |
includeUnlinkedX not a recognized boolean | leaderboard |
Unrecognized parameters are accepted and dropped, so a misspelled filter silently does nothing. order=asc is the common case: it is ignored, and the board stays descending.
There is no 401 or 403 on any of these routes, because no credential is checked. There is no 429.
Next: Client ID for how attribution is recorded in the first place.