Build a wallet PnL tracker on Solana with Birdeye Data. Snapshot net worth, plot the equity curve, and compute realized and unrealized profit per token.
July 22, 2026
A wallet PnL tracker answers the question a raw balance cannot: did this wallet make money, and on which tokens. A balance shows what a wallet holds right now, but it hides the cost paid to get there and ignores every trade already closed. This guide shows you how to build a wallet profit and loss (PnL) tracker on Solana using Birdeye Data, which returns current net worth, a dated equity curve, and realized and unrealized profit broken down token by token.
Birdeye Data covers three jobs here. It snapshots a wallet’s net worth and serves a historical value series for the equity curve. The same data layer computes realized and unrealized PnL for the whole wallet and every token in a single call. And it compares many wallets on one token so you can rank holders by profit. The whole tracker is four REST calls, two of which feed the same screen, and the response shapes nest deeper than you might expect, so most of the real work is reading the right field off the right path.
A wallet PnL tracker on Solana is three stages over four REST calls. Each stage answers one question about the money.

GET /wallet/v2/current-net-worth for net worth and holdings, and GET /wallet/v2/net-worth for the dated history that plots the equity curve.POST /wallet/v2/pnl/details, which returns a wallet summary and a per token tokens[] breakdown in one response.GET /wallet/v2/pnl/multiple, which returns PnL per wallet keyed by address.The two snapshot endpoints are Solana only. The two PnL endpoints also support twelve EVM chains, selected with the x-chain header. Every call uses the base URL https://public-api.birdeye.so with an X-API-KEY header. The rest of this guide expands each stage with parameters, a working request, and the response paths that are easy to read wrong.
The first screen of any wallet PnL tracker shows two things: what the wallet is worth right now, and how that worth got there. Net worth is the headline figure, and the equity curve under it is the trend. Two endpoints cover them, and they share the same wallet input, so you call both when the page loads.
Endpoint: GET /wallet/v2/current-net-worth and GET /wallet/v2/net-worth
Chains: Solana
Plan availability: All paid plans (Lite, Starter, Premium, Business, Enterprise)
Docs: current-net-worth, net-worth
Start with current-net-worth. It returns a total_value for the wallet and an items[] array of holdings, where each item carries address, symbol, amount, balance, decimals, price, and value. Pagination sits at the root of the response, next to data, with limit, offset, and total.
| Parameter | Required | Description |
|---|---|---|
x-chain | yes (header) | Chain, set to solana |
wallet | yes | Wallet address to value |
sort_by | no | value to order holdings by USD worth |
sort_type | yes | desc or asc |
filter_value | no | Minimum USD value for a holding to appear |
limit | no | Holdings per page, max 100 (default 20) |
offset | no | Pagination offset |
Two response details will bite you if you skip them. First, total_value, each holding’s value, and balance come back as JSON strings, not numbers, so adding them straight off the response concatenates text instead of summing. Parse each to a number before any maths. Second, balance is the raw on chain amount while amount is already scaled down, so a token with 6 decimals shows balance of 69000000 for an amount of 69. Display amount, or compute balance / 10^decimals yourself, and you avoid printing a holding inflated by a million times.
To keep the holdings list useful, sort by value with sort_type=desc so the largest positions sit at the top, and set filter_value to a small floor to drop dust tokens that would otherwise pad the list. A wallet holding hundreds of airdropped dust tokens becomes readable once you filter below a dollar or two.
curl --request GET \
--url '<https://public-api.birdeye.so/wallet/v2/current-net-worth?wallet=YOUR_WALLET&sort_by=value&sort_type=desc&limit=100>' \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'x-chain: solana'
Now add net-worth for the curve. It returns data.history[], where each point has a timestamp, a net_worth, a net_worth_change, and a net_worth_change_percent. The response also returns past_timestamp and current_timestamp to bound the range. One inconsistency to handle: net_worth here is a number, while total_value from the snapshot call is a string, so your parsing has to treat the two endpoints differently.
| Parameter | Required | Description |
|---|---|---|
x-chain | yes (header) | Chain, set to solana |
wallet | yes | Wallet address |
type | no | Point interval, 1h or 1d (default 1d) |
count | no | Number of points, max 90 (default 7) |
direction | no | back or forward from time (default back) |
time | no | ISO 8601 anchor timestamp |
sort_type | yes | desc or asc |
The parameter that shapes the curve is count, capped at 90 points. For a curve longer than 90 intervals, set time to the oldest timestamp you already hold, keep direction as back, and stitch each page onto the front of your series.
The type parameter sets the resolution. Use 1d for a long horizon curve where daily points are enough, and 1h when you want intraday detail over a shorter span. The two choices trade range against granularity, since the 90 point cap means hourly points cover under four days while daily points cover three months.
curl --request GET \
--url '<https://public-api.birdeye.so/wallet/v2/net-worth?wallet=YOUR_WALLET&type=1d&count=30>' \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'x-chain: solana'
To draw the curve, plot net_worth against timestamp in order, and use net_worth_change_percent to color each segment green or red by direction. Because the change fields are precomputed, you do not derive them from consecutive points, which keeps the chart logic simple.
The snapshot shows where the wallet stands and how its value moved. Two wallets can sit at the same value with very different track records behind them, one built on realized gains and one still riding a single unsold position. Step 2 tells those apart by computing PnL.
This is the engine of the tracker. One POST call returns both the whole wallet summary and the token by token breakdown, so a single request fills the win rate header and the PnL table at once. Realized PnL is the profit locked in on tokens already sold. Unrealized PnL is the paper gain or loss on what the wallet still holds.
Endpoint: POST /wallet/v2/pnl/details
Chains: Solana plus twelve EVM chains (set with x-chain)
Plan availability: All paid plans (Lite, Starter, Premium, Business, Enterprise)
Docs: Birdeye Data reference
The request is a POST with a JSON body, not query parameters. The body takes wallet, an optional token_addresses array to limit the breakdown to specific mints, and a duration window.
| Parameter | Required | Description |
|---|---|---|
x-chain | yes (header) | solana or one of twelve EVM chains |
wallet | yes (body) | Wallet address |
token_addresses | no (body) | Array of mints to restrict the breakdown |
duration | no (body) | all, 90d, 30d, 7d, or 24h (default all) |
limit | no (body) | Tokens per call, max 100 |
offset | no (body) | Pagination offset |
The duration window decides the period the PnL covers, from 24h up to all. Match it to the question you are asking: 24h for a day trader’s session, 30d or 90d for recent form, all for a full track record. Narrowing the window also trims the token list, since tokens untouched in the period drop out of the breakdown.
The response is where most of the integration work lives, because it nests several levels deep. The top level is data with three branches: meta, summary, and tokens[]. Each token in tokens[] carries its own nested objects: counts for trade counts, quantity for amounts including holding, cashflow_usd for invested and sold totals, pnl for the profit figures, and pricing for cost basis. The fields you render for the table are pnl.realized_profit_usd, pnl.unrealized_usd, pnl.total_usd, and pnl.avg_profit_per_trade_usd.
Inside each token, the other branches answer the follow up questions. quantity holds total_bought_amount, total_sold_amount, and the current holding, so you can show how much of a position is still open. cashflow_usd holds total_invested, total_sold, and current_value, the raw money in and out behind the profit figure. pricing holds avg_buy_cost and avg_sell_cost, the cost basis you put beside each token to show average entry and exit. Together these let the table explain a number rather than just print it.
The summary block mirrors that shape for the whole wallet. summary.pnl.total_usd is the headline figure for the wallet card, with summary.pnl.realized_profit_usd and summary.pnl.unrealized_usd splitting it into locked in and paper gains, and summary.unique_tokens counts how many tokens the wallet traded in the window. This is why one call fills two sections of the screen, the per token table and the wallet header, without a second request.
curl --request POST \
--url '<https://public-api.birdeye.so/wallet/v2/pnl/details>' \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'x-chain: solana' \
--header 'content-type: application/json' \
--data '{
"wallet": "YOUR_WALLET",
"duration": "30d",
"limit": 100
}'
Three things in this response trip up almost everyone. The payload has no success field, unlike the snapshot calls, so it returns straight data.{meta, summary, tokens[]}. If you guard on response.success before reading the body, you reject a perfectly valid response. Next, the wallet win rate is not at summary.win_rate. It lives at summary.counts.win_rate, alongside total_win and total_loss, so reading the shallow path returns undefined. Finally, pricing.current_price can be null for a token that is no longer priced, so any unrealized figure that depends on it needs a null check before display.
One limit shapes how you load a busy wallet. A single call returns at most 100 tokens, so a wallet that has traded more than that needs paging. Raise offset by your limit and call again until the pages run out, then merge them into one table before you sort.
With per token PnL computed, the tracker is complete for a single wallet. The last stage turns the same data outward, from one wallet across its tokens to many wallets on one token.
The final stage answers a different question: of everyone holding this token, who is up. You pass one mint and a list of wallets, and the call returns PnL for each wallet on that token, which is how you build a holder leaderboard or vet a list of copy trade candidates.
Endpoint: GET /wallet/v2/pnl/multiple
Chains: Solana plus twelve EVM chains (set with x-chain)
Plan availability: All paid plans (Lite, Starter, Premium, Business, Enterprise)
Docs: Birdeye Data reference
| Parameter | Required | Description |
|---|---|---|
x-chain | yes (header) | solana or one of twelve EVM chains |
token_address | yes | The single mint to compare on |
wallets | yes | Comma separated wallet list, max 50 |
Notice the parameter names flip from Step 2. Here you pass token_address (singular) and wallets (plural list), whereas pnl/details takes wallet (singular) and an optional token_addresses array. Send the wrong pair and the call rejects the request, so copy the names exactly.
curl --request GET \
--url '<https://public-api.birdeye.so/wallet/v2/pnl/multiple?token_address=TOKEN_MINT&wallets=WALLET_A,WALLET_B,WALLET_C>' \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'x-chain: solana'
The response shape is the gotcha here. It returns data.token_metadata with the token symbol and decimals, and data.data with the results. That inner data.data is an object keyed by wallet address, not an array, so you cannot call .map() or .sort() on it directly. Take Object.entries(data.data) to get wallet and stats pairs, then sort those by pnl.total_usd to rank the holders. Each wallet entry uses the same nested shape as a token from Step 2, so counts, quantity, cashflow_usd, pnl, and pricing all read the same way.
The wallets list caps at 50 addresses per call. To rank a larger holder set, split it into batches of 50, call once per batch, and merge the keyed results before sorting. Because the response is keyed by address, merging is a plain object spread with no duplicate rows to reconcile.
This is the call behind a smart money view. Rank the holders of a token by pnl.total_usd and the wallets at the top are the ones who timed it well, which is exactly the list you want to watch or follow. Run it on a token you are researching and you learn whether the biggest holders sit in profit or underwater, a faster read on conviction than holder counts alone.
A wallet PnL tracker that refreshes on every page view can run up calls quickly, and the pnl/details call is the heaviest of the four. Birdeye Data exposes a credits endpoint so you can read your own consumption directly and pace your refreshes against the real number.
Endpoint: GET /utils/v1/credits
curl --request GET \
--url '<https://public-api.birdeye.so/utils/v1/credits>' \
--header 'X-API-KEY: YOUR_API_KEY'
A practical pattern is to cache the snapshot and PnL response for a wallet and serve it for a short window rather than recomputing on every keystroke or tab switch. Watch the credits endpoint as your user count grows, and let that number set how aggressively you cache.
The four calls map onto two screens. The wallet view loads three calls in parallel and reads from one store. The compare view is its own call with its own ranking step.

Use this checklist before you ship:
x-chain: solana and an X-API-KEY.total_value, value, and balance are parsed to numbers before maths.amount, not the raw balance.pnl/details is read without a success guard, and win rate is taken from summary.counts.win_rate.pnl/multiple results are read with Object.entries(data.data), then sorted by pnl.total_usd.It is a tool that measures whether a wallet made money and where. A wallet PnL tracker reads net worth, a historical value curve, and realized and unrealized profit per token, then presents them as one view so you can judge performance rather than just balances.
The snapshot endpoints, current-net-worth and net-worth, are Solana only. The two PnL endpoints, pnl/details and pnl/multiple, also support twelve EVM chains, which you select with the x-chain header, so the PnL core travels beyond Solana even though the equity curve does not.
pnl/details returns straight data with meta, summary, and tokens[], with no success wrapper, so do not guard on response.success. The wallet win rate sits at summary.counts.win_rate, alongside total_win and total_loss, not at summary.win_rate.
Realized PnL is profit locked in on tokens the wallet has already sold, returned as pnl.realized_profit_usd. Unrealized PnL is the paper gain or loss on tokens still held, returned as pnl.unrealized_usd. The pnl.total_usd field combines both.
The results in data.data are an object keyed by wallet address, not an array. Convert it with Object.entries(data.data), then sort the pairs by pnl.total_usd to rank holders from most to least profitable on that token.
All paid plans, from Lite through Enterprise, per the accessibility listing on each endpoint reference page.
Each net-worth call returns up to 90 points, set by count, at hourly or daily intervals via type. For a longer curve, page backward by anchoring time to your oldest timestamp with direction set to back, then join the pages into one continuous series.
Net worth and PnL change only when the wallet trades or prices move, so refreshing every few seconds wastes calls. Cache each wallet’s snapshot and PnL for a short window, refresh the equity curve less often than the live price, and let the credits endpoint tell you when to widen those windows.
You now have every layer of a wallet PnL tracker, the net worth snapshot, the equity curve, per token realized and unrealized PnL, and a multi wallet comparison, all over REST. Grab an API key from Birdeye Data, start with the snapshot call, and add each stage until the full view is live.
Birdeye provides expansive data covering tokens, wallets, trades, and protocols across 300+ exchanges on 10 chains.
Whether you’re a solo tinkerer or a large team looking to scale, Birdeye offers plans that caters for your data needs and budget.
Dive into our docs and start querying data on 60+ APIs and 8 WebSocket types today!
Insights is a feature that allows users to analyze market trends in various aspects and dive deep into many industry sectors.
Find Gems is a feature that helps user identify potential Tokens at the current time.
Launch Explorer is a feature that enables users to access real-time data of tokens on popular launchpads like pump.fun, letsbonk.fun,...
New insight article by Birdeye reveals USDC's breakout growth in recent years
Data by Birdeye shows total trading volume of xStocks, PreStocks, and Ondo Global Markets on Solana peaked in March 2026
After the Drift Protocol's hack, Solana Foundation initiated programs such as STRIDE and SIRIN to tighten security for ecosystem teams

July 22, 2026
July 22, 2026