PearPear
API Integration

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.

EndpointSDK methodReturns
GET /statistics/volumesdk.core.statistics.volume()Volume, fees, trades, users, and new users, per time bucket.
GET /statistics/oisdk.core.statistics.oi()Open interest and open position count, per time bucket.
GET /statistics/totalssdk.core.statistics.totals()All-time volume, fees, trades, and users.
GET /statistics/leaderboardsdk.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.
  • clientId is 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.

ParameterRequiredRule
connectorYesOne of hyperliquid, binance, bybit, okx, lighter. One venue per call.
resolutionYes1h, 4h, or 24h. Selects the bucket width.
startDateNoISO 8601 date-time. Inclusive. Absent opens the range at the epoch.
endDateNoISO 8601 date-time. Inclusive. Defaults to now.
clientIdNoYour 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 }
  ]
}
FieldMeaning
timestampThe bucket's start, in epoch milliseconds.
volumeFilled notional in USD.
feesFees charged on those fills, in USD.
tradesFills in the bucket.
usersDistinct users who filled in the bucket.
newUsersUsers 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 newUsers never 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 }
  ]
}
FieldMeaning
timestampThe bucket's start, in epoch milliseconds.
openInterestMark-valued notional of the open positions, in USD.
openPositionsHow 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
  }
}
ParameterRequiredRule
connectorYesOne venue per call.
clientIdNoAbsent 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

ParameterDefaultRule
sortBytotalNetPnltotalNetPnl, volume, trades, or roi.
limit2001 to 200. Above 200 is rejected with 400, never clamped.
connectorevery venueRepeat the parameter or comma-separate it: connector=hyperliquid&connector=lighter.
assetevery assetSame repeat-or-comma form. Trimmed, and matched case-insensitively.
startDateunboundedUnix seconds, a non-negative integer. Inclusive.
endDateunboundedUnix seconds, a non-negative integer. Exclusive.
includeUnlinkedXfalsetrue or false. See below.
clientIdevery clientYour 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.

EndpointFormatExample
/statistics/volume, /statistics/oiISO 8601 date-timestartDate=2026-08-01T00:00:00Z
/statistics/leaderboardUnix secondsstartDate=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:

GroupRule
volume, totalTradesEvery counted fill inside the window, whatever period its position opened in.
grossRealizedPnl, grossUpnl, pearFee, tradeFee, roiOnly 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

FieldTypeMeaning
tradeAccountIdstringThe row's identity. Render or join by this.
userIdstringThe trader behind the account, so you can group their rows across venues.
connectorstringThe account's venue.
volumedecimal stringFilled notional in the window.
grossRealizedPnldecimal stringRealized PnL before fees.
grossUpnldecimal stringUnrealized PnL, marked now. "0" once the window has closed.
fundingPaymentdecimal stringNet funding carry, signed. Account-level.
totalNetPnldecimal stringRealized, less both fees, plus funding, plus unrealized.
pearFeedecimal stringPear's fee on the counted fills.
tradeFeedecimal stringThe venue's fee on the counted fills.
totalTradesnumberCounted fills in the window.
roidecimal string, optionalAverage per-position return, as a ratio. Absent when unmeasurable.
xHandlestring, optionalLinked X handle. Absent if unlinked or hidden.
xNamestring, optionalLinked X display name.
xAvatarUrlstring, optionalLinked X avatar.
xUserIdstring, optionalImmutable 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.

EndpointclientId=other
/statistics/volumeThe unattributed series. Matched case-insensitively.
/statistics/oiThe unattributed series. Matched case-insensitively.
/statistics/totalsThe 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.

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

CauseEndpoints
connector missingvolume, OI, totals
connector not one of the five venuesall four
resolution missing, or not 1h, 4h, or 24hvolume, OI
startDate at or after endDatevolume, OI, leaderboard
startDate or endDate not an ISO 8601 date-timevolume, OI
startDate or endDate negative, fractional, or not a numberleaderboard
clientId empty or whitespace onlyvolume, OI, totals
limit above 200, zero, negative, or fractionalleaderboard
sortBy not one of the four metricsleaderboard
asset present but whitespace onlyleaderboard
includeUnlinkedX not a recognized booleanleaderboard

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.

On this page