Auditing a Crypto Trader's PnL from Raw API Data — A Step-by-Step Procedure
How to reproduce a crypto trader's claimed profit-and-loss figure from raw exchange API data. Pulls, canonicalisation, NAV reconstruction, fee handling, and TWR re-derivation explained.
- Reproducing a crypto trader's claimed PnL means starting from raw exchange API responses and ending at the published figure, without trusting any intermediate dashboard.
- The procedure has six discrete steps: credential setup, raw data pull, canonicalisation, NAV reconstruction, fee and funding adjustment, and TWR computation.
- The most common failure mode is mismatched fee handling — what an exchange shows as P&L on its dashboard is gross of fees and funding rates the trader actually paid.
- On NakedPnL the procedure runs every day automatically — daily NAV from venue API, canonicalised, hashed, chained. A reviewer can re-run any specific day's pull and confirm.
A crypto trader's published PnL is a number on a screen. Behind it is — or should be — a stream of raw exchange API responses that, processed through a documented methodology, produces exactly that number. Auditing the published figure means starting from those raw responses and arriving at the same number without trusting the trader's dashboard, the venue's rendered figure, or any intermediate calculation. The procedure is mechanical, not subjective; either the published figure reproduces from primary records or it does not, and a clean audit gives a yes-or-no answer.
This guide walks the procedure end to end with the level of detail that an auditor with access to read-only API credentials needs. It covers the things that go wrong in practice — fee accounting, funding-rate handling, deposit and withdrawal classification, derivative-vs-spot reconciliation — and explains why each one matters for the final figure. The how-to-verify-a-trader-track-record-yourself guide covers the broader six-step verification framework; this guide narrows in on the crypto-exchange-specific data pull and arithmetic.
Step 1 — Credential setup and the trust model
An audit of a trader's account requires either the trader's read-only API credentials or a continuously-maintained chain at a verification registry that already pulled them. The credential's permissions should be limited to read — Binance's case for this is a credential with no withdrawal or trading permission, no IP whitelist requirement that prevents the auditor from making the call, and access to the account information and trade history endpoints. Bybit, OKX, and Kraken expose comparable read-only options. An auditor never asks for full-permission credentials. If the trader will not produce a read-only credential, the audit cannot proceed.
On NakedPnL the credential is registered once at onboarding and the daily snapshot cron at 23:55 UTC pulls the necessary endpoints. The audit-from-raw-data path is the same code path that produced the chain — but the auditor can also run it independently by calling the same exchange endpoints with the same credentials and confirming that the response matches the canonical record stored in the chain bundle.
Step 2 — Raw API pull
Each exchange exposes a different surface for account-level state. The audit-relevant endpoints divide into three categories: balance and equity (the per-asset and total account state at a moment in time), trade history (every executed order with price, size, fee, and timestamp), and ledger (every credit, debit, transfer, funding-rate accrual, and fee in time order). For an audit, all three are necessary because PnL reconstruction requires reconciling balance changes against trade activity and explaining any residual through the ledger.
| Exchange | Balance/equity | Trade history | Ledger |
|---|---|---|---|
| Binance Spot | GET /api/v3/account | GET /api/v3/myTrades (per symbol) | GET /sapi/v1/asset/transfer + capital/withdraw + capital/deposit |
| Binance Futures | GET /fapi/v2/account | GET /fapi/v1/userTrades | GET /fapi/v1/income (FUNDING_FEE, COMMISSION, TRANSFER, REALIZED_PNL) |
| Bybit Unified | GET /v5/account/wallet-balance | GET /v5/execution/list | GET /v5/account/transaction-log |
| OKX | GET /api/v5/account/balance | GET /api/v5/trade/orders-history | GET /api/v5/account/bills |
Pulls should be paginated to completion — exchange APIs cap responses (typically 500 to 1000 rows per call) and the auditor must walk the cursor or time-window pagination to ensure no records are missed. The most common audit failure at this step is silent truncation: the auditor pulls 1000 trade records, sees that one or more are at the page boundary, and forgets that the original history may extend past it. A complete pull verifies the record count against the exchange's reported totals (where available) and proceeds to the next step only when the dataset is closed under all three endpoint categories for the audit period.
Step 3 — Canonicalisation
Raw exchange responses are JSON with potentially-different whitespace and key ordering across calls. A bare hash of the wire response is brittle — two semantically identical responses with different whitespace would hash differently. The fix is canonicalisation: serialise each response with sorted keys and no whitespace, then hash the canonical form. This is the same procedure NakedPnL runs in lib/calculation/audit-hash.ts. The canonical-json-canonicalization-financial-records guide covers the protocol details.
Why this matters for an audit: when the auditor's pull is compared to the chain's stored response, the comparison is on canonical form, not raw bytes. If the canonical forms match, the audit confirms that the response captured at the original snapshot time is identical to what the auditor pulls now. If they differ, the discrepancy is either a real change in the underlying data (which would itself be diagnostic — exchange-side restatements are unusual but happen) or a canonicalisation bug. Either is investigable.
Step 4 — NAV reconstruction
NAV at any moment is the dollar (or stablecoin) value of all assets in the account, plus or minus mark-to-market on open derivative positions. Reconstructing NAV from raw responses requires three inputs: the per-asset balance from the balance endpoint, the mark prices for any spot assets in non-quote currency, and the unrealised P&L on any open derivative positions. For a USDT-quoted derivatives account, this simplifies to wallet balance + unrealised P&L from the futures account endpoint. For a multi-asset spot account, the per-asset balances must be priced against a reference (USDT or USD) using exchange-published mark prices at the snapshot time.
The reconstruction is daily-cadence on NakedPnL: the snapshot at 23:55 UTC fetches account state at a defined moment, prices the multi-asset case against exchange-published mark prices, and stores the resulting equity figure as the NavSnapshot row. An auditor reproducing the figure pulls the same endpoints with timestamps within the snapshot window and arrives at the same NAV to within sub-cent precision (Decimal.js, 28 digits). If the figures diverge, the divergence is either price-source (different mark used) or timing (auditor pulled at a different moment) and can be reconciled by aligning the inputs.
Step 5 — Fee and funding-rate adjustment
This is where most casual audits fail. An exchange's dashboard typically displays gross PnL — the difference between exit price and entry price scaled by position size — without subtracting trading fees, funding-rate accruals, or borrow costs. An honest audit must net all of these, and they are non-trivial. On Binance USDT-M Futures, funding rates are paid every 8 hours and accrue to the income endpoint as FUNDING_FEE entries. Trading fees appear as COMMISSION entries. On a high-frequency strategy these can dominate the gross P&L; an unadjusted audit would over-state realised performance by orders of magnitude.
from decimal import Decimal, getcontext
getcontext().prec = 28
def account_pnl_from_ledger(ledger_entries, opening_equity, closing_equity):
"""Reconstruct net account PnL from primary ledger.
ledger_entries: list of {type, asset, amount} from venue ledger endpoint.
opening_equity, closing_equity: equity at start/end of audit period.
Returns dict with components so the auditor can see
where the period's PnL actually came from.
"""
deposits = sum(Decimal(e['amount']) for e in ledger_entries
if e['type'] in ('TRANSFER_IN', 'DEPOSIT'))
withdrawals = sum(Decimal(e['amount']) for e in ledger_entries
if e['type'] in ('TRANSFER_OUT', 'WITHDRAWAL'))
net_flow = deposits - withdrawals
realized_pnl = sum(Decimal(e['amount']) for e in ledger_entries
if e['type'] == 'REALIZED_PNL')
funding = sum(Decimal(e['amount']) for e in ledger_entries
if e['type'] == 'FUNDING_FEE')
fees = sum(Decimal(e['amount']) for e in ledger_entries
if e['type'] == 'COMMISSION')
# Sanity check: the equity change net of flows equals
# realized + unrealized + funding + fees.
derived_change = closing_equity - opening_equity - net_flow
components_total = realized_pnl + funding + fees
# The residual is unrealized PnL change on open positions.
unrealized_change = derived_change - components_total
return {
'realized_pnl': realized_pnl,
'funding': funding,
'fees': fees,
'unrealized_change': unrealized_change,
'net_flow': net_flow,
'derived_change': derived_change,
}The reconstruction surfaces the components separately. A trader claiming '40% return last quarter' on a strategy that paid 8% in funding, 3% in fees, and made 51% gross trading P&L has produced an honest 40% net figure. A trader claiming the same 40% on a strategy that paid 25% in funding and made 65% gross has produced a much riskier set of returns and the picture is qualitatively different. Decomposing into components changes how the figure is interpreted even when the headline number is correct.
Step 6 — TWR computation
Once NAV is reconstructed for each snapshot date and external cash flows are isolated from realised P&L (Step 5), TWR is the geometric chain-link of sub-period returns. The methodology guide on time-weighted return walks the algorithm; the canonical implementation is in lib/calculation/twr-engine.ts and the auditor's reference is at /docs/verification with both Python and JavaScript snippets. The arithmetic is straightforward; the discipline is in identifying every flow correctly and using arbitrary-precision decimals so the chain-link does not accumulate floating-point error across hundreds or thousands of compounded daily steps.
An auditor running this step should expect to reproduce a NakedPnL TWR figure to 28-decimal precision when the inputs (canonical NAV series, identified flows) are aligned. Any divergence at higher than the eighth decimal indicates a flow misidentification or a precision bug; either is investigable. A divergence at the third decimal is a real disagreement and the figure does not reconcile.
What an audit cannot solve
An audit reproduces what the venue records say happened in a connected account. It cannot detect un-connected accounts the trader also operates, cannot rule out wash-trading or self-trading patterns the venue itself has not flagged, and cannot anticipate strategy changes the trader will make in the future. The audit's scope is exactly: for the connected accounts, over the connected period, do the published figures reproduce from the venue's primary records. That narrow result is what verification is supposed to deliver. The methodology guide on survivorship bias in trader rankings covers what an audit does not address.
Crypto venues themselves can also restate. An exchange that detects errors in past funding-rate accruals will sometimes issue retroactive adjustments. Honest audit procedure flags any such restatement explicitly rather than re-syncing it silently into the chain — the chain entry remains as it was at the original snapshot, and a corrective entry is appended with both visible. The integrity of the audit trail depends on this: a chain that silently restates loses the property that the historical record cannot be retroactively edited.
How NakedPnL automates the audit
NakedPnL runs the six-step procedure every day for every connected venue account. The daily snapshot cron pulls account state at 23:55 UTC; the response is canonicalised; the canonical form is SHA-256 hashed; the content hash plus the response is stored as a NavSnapshot row; the chain header is computed as SHA-256(previous chain header + content hash); the daily Merkle root of all chain heads is committed to Bitcoin via OpenTimestamps. The TWR engine runs over the canonical NAV series whenever the published figure is recomputed. Each step is open-source and documented; the chain bundle is exposed at the chain API for any third party to download and re-derive.
A reviewer who wants to check a specific day's figure does not need to run the full audit from scratch. The chain bundle has the canonical response for that day; the auditor pulls the same endpoints with the same credentials at any time and confirms that the canonical forms match. The TWR re-derivation is then a single function call against the verified NAV series. The work scales to any reviewer at near-zero marginal cost — exactly the property a verification surface needs.