Solutions · Portfolio trackers
MadeOnSol is the wallet activity layer for Solana portfolio apps: swaps and SOL transfers of watched wallets as structured events, on-chain holdings with the balance change no trade explains, cost-basis PnL on the launchpad trades it captures, and KOL, bot and deployer labels for counterparties and tokens, while valuation, accounting and the interface stay in your app.
An address and a balance do not tell a user what happened. Turn each transaction into a row they can read: what was bought or sold, how much SOL moved, and whether the token or the wallet on the other side carries a label worth a second look.
A Free key reads GET /token/{mint}. The Wallet Tracker, its webhook, positions, PnL and counterparty labels from Pro; holdings and the wallet_tracker:events channel on Ultra. Showing the data to your own users needs Business.
What one activity row needs
Four questions a user asks about a transaction, and the call that answers each.
What happened
wallet_tracker:eventProevent_typeactionsol_amounttoken_amount
Which token
GET /token/{mint}Freenamesymbolprice_usddeployer.tier
Who was on the other side
POST /wallet/batch/classifyProis_kolkol_nameis_sniperbot_confidence
Is the balance explained
GET /wallet/{address}/holdingsUltraamounttrade_derived_amounttransfer_delta
The problem
A portfolio screen has to explain activity, not list it. That means parsing every venue's swaps into a side and an amount, telling a trade from a transfer, keeping rows in chain order when a stream reconnects, spotting tokens that arrived without a trade, and knowing which counterparties and tokens deserve a warning. Each of those is its own indexer before the first row renders.
Show me
The events are what MadeOnSol delivers; the context calls fill in what the event does not carry; the last step is your code. Field names are the real keys, values are illustrative and the bodies are abridged.
A buy and a SOL transfer on a watched wallet. The swap carries market cap change and volume for its token; the transfer names the other side but not the direction.
[
{
"id": "wallet_tracker:event:4vJ9...Qm2x",
"seq": 512034,
"channel": "wallet_tracker:events",
"event": "wallet_tracker:event",
"data": {
"subscriber": "3f1c...9a07",
"wallet_address": "7xKX...3bPq",
"label": "Main wallet",
"event_type": "swap",
"action": "buy",
"token_mint": "8vdc...pump",
"sol_amount": 1.25,
"token_amount": 482113.52,
"counterparty": null,
"tx_signature": "4vJ9...Qm2x",
"block_time": 1790330520,
"slot": 449812044,
"replayed": false,
"ts": "2026-09-25T10:02:00.000Z",
"mc_change_pct": { "5m": 12.4, "15m": 30.1 },
"volume_usd": { "5m": 8420.5, "15m": 21980 },
"mev_volume_pct": { "5m": 4.2, "15m": 3.8 },
"history_age_seconds": 1140
},
"ts": 1790330520000
},
{
"id": "wallet_tracker:event:2mLq...Xw9e",
"seq": 512041,
"channel": "wallet_tracker:events",
"event": "wallet_tracker:event",
"data": {
"subscriber": "3f1c...9a07",
"wallet_address": "7xKX...3bPq",
"label": "Main wallet",
"event_type": "transfer",
"action": null,
"token_mint": null,
"sol_amount": 12,
"token_amount": null,
"counterparty": "9WzD...AWWM",
"tx_signature": "2mLq...Xw9e",
"block_time": 1790330581,
"slot": 449812197,
"replayed": false,
"ts": "2026-09-25T10:03:01.000Z"
},
"ts": 1790330581000
}
]One batch call labels the transfer's counterparty, and up to 100 more in the same request.
POST /api/v1/wallet/batch/classify
Authorization: Bearer msk_...
Content-Type: application/json
{ "wallets": ["9WzD...AWWM"] }
// 200
{
"wallets": [
{
"address": "9WzD...AWWM",
"is_sniper": true,
"is_bundler": false,
"is_dumper": false,
"is_kol": false,
"kol_name": null,
"bot_confidence": "high",
"dump_cluster": null
}
],
"count": 1,
"as_of": "2026-09-25T10:03:02.410Z"
}The event has the mint only. GET /token/{mint} (Free) adds the name, symbol, price and the deployer's tier.
// GET /api/v1/token/8vdc...pump
{
"token": {
"mint": "8vdc...pump",
"name": "New Token",
"symbol": "NEW",
"image_url": "https://.../new.png",
"price_usd": 0.0000912,
"market_cap": 91200,
"liquidity_usd": 18400,
"price_is_stale": false,
"is_blacklisted": false,
"deployer": {
"wallet": "8kP4...Qe2x",
"tier": "good",
"bonding_rate": 0.41,
"total_deployed": 34,
"total_bonded": 14
}
},
"as_of": "2026-09-25T10:02:01.120Z"
}Your code: one row per wallet per transaction, sorted by slot, recovered events marked, labels as badges.
import type { WalletTrackerTrade } from "madeonsol-x402";
type Token = { symbol: string | null; deployer: { tier: string } | null };
type Party = { is_kol: boolean; kol_name: string | null; is_sniper: boolean; bot_confidence: string | null };
function toActivityRow(e: WalletTrackerTrade, token?: Token, party?: Party) {
const badges: string[] = [];
if (token?.deployer) badges.push(`deployer: ${token.deployer.tier}`);
if (party?.is_kol) badges.push(`KOL: ${party.kol_name}`);
if (party?.is_sniper) badges.push("sniper wallet");
return {
key: `${e.tx_signature}:${e.wallet_address}`, // frame id is tx_signature only
order: e.slot, // chain order, not arrival
recovered: e.replayed, // mark, never re-notify
title: e.event_type === "swap"
? `${e.action === "buy" ? "Bought" : "Sold"} ${token?.symbol ?? "a token"}`
: "SOL transfer",
detail: e.event_type === "swap"
? `${e.sol_amount} SOL`
: `${e.sol_amount} SOL with ${e.counterparty ?? "unknown"}`,
badges,
};
}
// → { key: "4vJ9...Qm2x:7xKX...3bPq", title: "Bought NEW", detail: "1.25 SOL", badges: ["deployer: good"] }
// → { key: "2mLq...Xw9e:7xKX...3bPq", title: "SOL transfer", detail: "12 SOL with 9WzD...AWWM", badges: ["sniper wallet"] }one row per wallet per transaction
The tracker stores one event per transaction, watchlist and wallet, but the WebSocket frame id is keyed on tx_signature alone. A transfer between two of a user's own watched wallets is two events with the same id, so dedupe rows on tx_signature and wallet_address together, never on the frame id.
replayed
A row recovered after a reconnect arrives with replayed: true and its original slot. Show it in its place in the list; do not send a new notification for it.
rest.walletTrackerTrades({ order: "slot" })rest.walletClassify(counterparties)rest.walletHoldings(address)client.wallet_tracker_trades()client.wallet_holdings(address)madeonsol_wallet_holdingsTry it
The free wallet scanner shows PnL, the KOL, alpha-wallet and deployer labels, and the latest trades for any Solana address, with no key. Then call the same data from your backend.
From wallet event to portfolio activity
The first four stages run on MadeOnSol and return named fields; the last two are your product. Context is optional: a row works with the event alone, and gets its token name and labels from two extra calls.
01 · MadeOnSol
Your app puts the address on its watchlist with the user's own label. Tracking starts at that moment; earlier transactions are not backfilled into the watchlist.
POST /wallet-tracker/watchlist
wallet_addresslabel02 · MadeOnSol
A token balance change with a SOL leg becomes a swap with a side; a USDC or USDT leg is converted to its SOL equivalent. A SOL movement of at least 0.001 SOL with no token change becomes a transfer with the counterparty it could match.
event_typeactiontoken_mintsol_amountcounterparty03 · MadeOnSol
Poll the stored rows, receive each event as a signed webhook, or hold the stream open. The live event also carries the token's recent market cap change and volume when MadeOnSol prices that mint.
GET /wallet-tracker/tradeswallet_tracker:event webhookwallet_tracker:events channel
slotreplayedmc_change_pctvolume_usd04 · MadeOnSol
One call names the token and its deployer tier; one batch call labels up to 100 counterparties as KOL, sniper, bundler or dumper, with a bot-confidence rating. Neither is on the event itself.
GET /token/{mint}POST /wallet/batch/classify
symboldeployer.tieris_kolbot_confidence05 · Your app
Your code turns the event and its context into a sentence, a key and badges: one row per wallet per transaction, sorted by slot, with recovered history marked instead of announced.
your row keyyour wordingyour badges06 · Your app
Grouping, valuation in the user's currency, notifications and the layout are your product. MadeOnSol supplies the facts behind each row.
your valuationyour UIChips are the response or event fields each stage returns. Wallet Tracker APIWallet API
The event, field by field
The stored row (REST) and the live event (webhook and WebSocket) are the same event with two differences: the live one adds the token's recent market data, and only the stored row has the unused name columns.
| Field | REST row | Live event | What it means for a row |
|---|---|---|---|
| event_type · action | yes | yes | swap with buy or sell, or transfer with action null. |
| sol_amount | yes | yes | The wallet's SOL change, fees included. On a transfer it is the absolute amount: the event does not say whether SOL came in or went out. |
| counterparty | transfers only | transfers, else null | The account whose SOL change matched this one. Absent from the REST row on swaps. |
| slot · replayed | yes | yes | Chain position, and whether the event came through recovery after a reconnect. |
| mc_change_pct · volume_usd · mev_volume_pct | no | when the mint is priced | Market cap change and organic volume per window at the moment of the event. |
| token name and symbol | not filled (always null) | no | Not available on the event. token_symbol and token_name appear on the stored row but are never written; read the name and symbol from GET /token/{mint}. |
| program or venue | no | no | The event does not name the DEX or program the swap went through. |
| KOL, alpha or bot labels | no | no | Labels are a separate lookup on the wallet or counterparty address. |
Balances and results
Holdings come from the chain; positions and PnL come from the trades MadeOnSol captured. Where they disagree, that is information: a token the wallet holds but never bought through a captured trade shows up as transfer_delta.
GET /wallet/{address}/holdingsCurrent balances read from the chain (SPL and Token-2022 accounts plus native SOL), priced from MadeOnSol's token index, with the part of each balance no captured trade explains.
total_value_usd adds up the priced tokens only; sol_balance stays in SOL. Cached 90 seconds, 6 uncached lookups per minute.
sol_balancevalue_usdtransfer_deltasummary.priced
GET /wallet/{address}/positionsOpen lots rebuilt first-in first-out from the wallet's captured trades, with cost basis, average entry and unrealized result in SOL.
Trade-derived, so a token received by transfer is not a position. Launchpad trades from the last 90 days.
cost_basis_solavg_entry_price_solunrealized_sol
GET /wallet/{address}/pnlRealized and unrealized result in SOL, win rate, profit factor, hold times, drawdown, a daily PnL curve and every closed position.
cost_basis_observable_from says where the data starts; a sell whose buy predates it is left out rather than guessed.
realized_solunrealized_solpnl_curvecost_basis_observable_from
NEW was bought through a captured trade, so its delta is 0. DROP's captured trades net to 0 while the wallet holds 300,000, so that balance arrived by transfer or through a swap outside the captured pipeline. The third token has no MadeOnSol price, so it is left out of total_value_usd, and so is the SOL balance.
// GET /api/v1/wallet/7xKX...3bPq/holdings
{
"address": "7xKX...3bPq",
"sol_balance": 3.412,
"holdings": [
{
"mint": "8vdc...pump",
"symbol": "NEW",
"name": "New Token",
"amount": 482113.52,
"price_usd": 0.0000912,
"value_usd": 43.97,
"is_bonded": false,
"trade_derived_amount": 482113.52,
"transfer_delta": 0
},
{
"mint": "Hx3r...pump",
"symbol": "DROP",
"name": "Drop",
"amount": 300000,
"price_usd": 0.0000042,
"value_usd": 1.26,
"is_bonded": true,
"trade_derived_amount": 0,
"transfer_delta": 300000
},
{
"mint": "Zq81...xYt4",
"symbol": null,
"name": null,
"amount": 25,
"price_usd": null,
"value_usd": null,
"is_bonded": null,
"trade_derived_amount": null,
"transfer_delta": null
}
],
"summary": { "token_accounts": 4, "non_zero": 3, "returned": 3, "priced": 2, "total_value_usd": 45.23, "truncated": false },
"verified_at": "2026-09-25T10:05:12.000Z",
"trade_window_days": 90,
"cache_hit": false,
"ttl_seconds": 90
}Boundaries
MadeOnSol provides wallet and onchain activity and the intelligence around it; valuation and accounting stay with your app, except for two verified capabilities: holdings value_usd for tokens in MadeOnSol's index only, and FIFO cost-basis PnL in SOL over the launchpad trades captured in the last 90 days. A complete history of every transaction also stays with your app.
| Topic | What MadeOnSol provides | What stays with your app |
|---|---|---|
| Total portfolio value | Prices the memecoin and launchpad tokens it indexes. Holdings add those up; SOL is returned as a SOL balance, and stablecoins, wrapped SOL and other assets outside the index come back unpriced. | Your own price source for SOL, stablecoins and major assets, and the currency conversion. |
| Accounting and tax | FIFO cost basis in SOL over the launchpad trades captured in the last 90 days. No fiat cost basis, tax lots, fee breakdown or reports. | Cost basis in the user's currency, lot rules, fees and anything a tax report needs. |
| Every transaction a wallet made | The watchlist starts when a wallet is added and keeps events 120 days. Trade history covers the launchpad pipeline from 2026-04-12. Token transfers without a SOL or stablecoin leg, stablecoin transfers and SOL moves under 0.001 SOL are not events. | Your own store of activity beyond the retention window, and your RPC provider for transfer direction and anything outside the pipeline. |
| Funding history of your users' wallets | Funding evidence is collected for MadeOnSol's public tracked set: active KOL wallets and elite or good deployers. Customer watchlists are deliberately outside it. | Where a user's money came from, if your product needs it. |
| Custody and execution | Reads data. It never holds keys, signs or sends transactions. | Wallet connection, signing and anything that moves funds. |
Live monitoring and plans
Wallet endpoints start on Pro; holdings and the live wallet_tracker:events channel on Ultra. A user-facing app also needs the display licence noted under the build list.
| Your app needs | Use | Plan |
|---|---|---|
| Activity feed for watched wallets | GET /wallet-tracker/tradesSlot order by default; page with before_slot. | Pro |
| History for a wallet a user just added | GET /wallet/{address}/tradesCaptured launchpad trades, last 90 days by default; older months come from the archive. | Pro |
| A notification when a watched wallet moves | wallet_tracker:event webhookDelivered only to the account whose watchlist produced it; min_sol and action filters apply. | Pro |
| Live rows while the app is open | wallet_tracker:events channelSubscriber-scoped on /ws/v1/stream. | Ultra |
| Live value of the tokens a user holds | token:prices channelMint-scoped; mints per connection: Pro 25 · Ultra 100 · Business 250. | Pro |
| Price-drop alerts on held tokens | POST /price-alertsdrop_pct and an optional recovery_pct per token; delivered by webhook or the price_alert:events channel. | Pro |
| Current balances | GET /wallet/{address}/holdingsRead from chain, cached 90 seconds. | Ultra |
| More wallets than one watchlist holds | POST /wallet/batch/tradesUp to 50 wallets per call; feed next_since back as since to poll only new trades. Watchlist sizes: Pro 50 · Ultra 100 · Business 500. | Pro |
Compared
Both start from the same events. Wallet Tracking covers watching addresses and delivering their events to your backend; this page covers what a user sees once those events reach your app. The data layer behind both is described in the data dictionary.
| Aspect | Wallet Tracking | Portfolio Trackers |
|---|---|---|
| Starts from | A list of addresses your backend watches. | The wallets a user connects, and what they expect to see about them. |
| Main output | Swap and transfer events delivered to your system. | Readable activity rows, balances and results, with labels for the tokens and the other side. |
| The question it answers | What did these wallets just do? | What happened in my wallet, and should I look twice at any of it? |
| MadeOnSol surfaces | Watchlist, the events feed, webhooks, the wallet_tracker:events channel. | The same events plus holdings, positions, PnL, token and counterparty context, token:prices and price alerts. |
Architecture
1 · Source
Chain activity
Transactions and program events
2 · MadeOnSol
Data and intelligence layer
3 · Delivery
4 · Yours
Your activity rows, valuation and accounting
Keys and business logic stay server-side
5 · Users
Your portfolio app
Build outcomes
Showing MadeOnSol data to your own users requires a Business plan. See pricing
Keep going
Solution
Wallet Tracking
The watchlist and event delivery behind the activity feed.
Solution
Token Risk
Risk evidence for the tokens in a user's wallet.
Product
Wallet scanner
PnL, labels and recent trades for any address, no key.
Guide
Consume wallet_tracker:event webhooks
Payload fields, signature check, dedupe per wallet and transaction.
FAQ
Next step
Scan a wallet to see the data, check what each dataset covers, then build the activity feed on the wallet API.