PnLol / Method
How the engine computes P&L (method of record)
Software, not tax, legal or investment advice. Estimates may be wrong. Verify with a tax professional.
Pipeline
- History (
engine/src/fetch-wallet.js):getSignaturesForAddress(wallet)PLUS the signatures of every token account the wallet owns (current ones fromgetTokenAccountsByOwner, closed ones seen in its transactions, up to 3 rounds). A transfer into an existing token account never mentions the owner wallet, so the wallet's own signature list alone misses it (measured: a pump.fun wallet reconciled 11/15 mints without this, 15/15 with it). Free tier: most recent 500 wallet signatures. - Normalize (
engine/src/normalize.js): per transaction, the BALANCE DELTAS of everything the wallet owns. No instruction decoding, so every DEX (pump.fun, PumpSwap, Raydium v4/CPMM/CLMM/LaunchLab, Meteora DLMM/DAMM/DBC, Orca, Jupiter routes) is handled the same way. SOL = wallet lamports + lamports of its own token accounts (wrapped SOL folds in; rent the wallet pays into its own accounts and gets back on close nets to zero). Failed transactions = fee only. A sliver of SOL (< 0.01) next to a pure token transfer is a cost (rent for the recipient's account / tips); next to a token<->token trade it is a cost if paid, ignored if received. Program label = the aggregator if present (Jupiter), else launchpad, else AMM. - Price (
engine/src/prices.js): SOL/USD = Coinbase Exchange 1-minute candle close at the minute of the transaction (nearest minute within 30 min if empty). Stablecoins (USDC, USDT, PYUSD, USD1) = $1. A trade against SOL or a stablecoin gives the other leg the same USD value. Token<->token swaps with no SOL/stable leg are priced from the wallet's own nearest priced trade of the same token within 24 h (flaggedprice_from_own_trade), else flaggedunpriced. Current prices: DexScreener, highest-liquidity pair. - Basis (
engine/src/basis.js): FIFO lots per mint, SOL included.
Tax conventions (US federal; each is a choice a user may need to change)
| Situation | Treatment | Source / reason |
|---|---|---|
| Buy a coin with SOL | SOL disposal (amount = SOL traded + fee SOL; amount realized = USD value of the coin received). Coin basis = its USD value, no fee added. | IRS digital-asset FAQ Q66/Q72 (added 2025-12-15): in a crypto-for-crypto exchange, transaction costs are allocable to the asset given up and may not be added to the basis of the asset received. |
| Sell a coin for SOL | Coin amount realized = USD value of SOL received minus fee. SOL lot = net SOL received at that value. | FAQ Q65/Q66 (costs reduce amount realized). |
| Token -> token swap | Given-up token: amount realized = value received minus fee; received token basis = its value; fee SOL disposed at its value. | FAQ Q62/Q66/Q72. |
| Buy with a stablecoin | Treated like a cash purchase: fee added to basis; stablecoin not tracked as a lot. Flag stablecoin_treated_as_cash. | Simplification (FAQ Q56 is for cash); stablecoin gain/loss assumed ~0. |
| Failed tx / account close / fee-only | Fee SOL is a SOL disposal at its USD value. | FAQ Q62 (paying for services with digital assets is a disposition). Produces many tiny rows. |
| SOL arriving by plain transfer | Lot at USD value on arrival, flagged sol_basis_is_value_on_arrival. | Its real cost was paid elsewhere (exchange). User must supply real basis for accurate SOL gains. |
| Token arriving by transfer | Cost $0, flagged missing_basis (+ airdrop_possible_income if the wallet did not sign). | Cost not visible. Airdrop income (Rev. Rul. 2019-24) NOT computed. |
| Token sent out by transfer | Not a sale; its FIFO lots leave the wallet ("sent out (cost)"). | Assumed to be the user's own other wallet. Gifts/payments would be wrong. |
| Burn | Disposal for $0, flagged burned. | Treatment of worthless/abandoned tokens is unsettled; see the rugged-token guide. |
| Coin held with no market | Shown at $0 value (unrealized loss), flagged possibly_worthless; NOT a realized loss. | CCA 202302011: a price collapse alone is not a deductible loss. |
| Holding period | Long-term only if the disposal date is after the 1-year anniversary date (UTC dates). | 26 USC 1222 ("more than 1 year"). UTC, not the taxpayer's local date. |
Several wallets (engine/src/multiwallet.js)
Up to 3 addresses of the same person (free), up to 10 with a Tax Season Pass. Results include a per-wallet view (byWallet: realized short/long-term per wallet; each disposal row carries the wallet it came from). If one wallet cannot be loaded the others are still reported, with an explicit warning that the failed wallet is NOT in the totals (walletErrors). A transaction where tokens leave one of them and the same mint and exact raw amount arrive in another is a self_transfer: no sale, and the FIFO lots (cost and acquisition date) move with the tokens. The live tool uses lotScope: 'wallet' (one FIFO queue per wallet and coin) because Treas. Reg. 1.1012-1(j) requires wallet-by-wallet basis identification for dispositions from 2025 on; 'universal' pooling is available but not used on the site.
Lot-selection method (engine/src/lotselect.js)
FIFO (default), HIFO (highest cost per unit first) and LIFO are available on the dashboard and in exports. The method is the user's choice; the product does not recommend one, and the method is printed in every export (x-pnlol-method, report method). The same method applies to the lots that leave with a transfer. Specific identification (picking lots by hand) is not offered. Whether a given method is allowed for a given taxpayer and year is a question for a tax professional.
Report readiness (engine/src/readiness.js)
A report is ready when the review queue is empty and no disposal is unpriced. Exports whose format tolerates a comment line (Form 8949-style, its summary, generic) start with # N unresolved items ... until then; every export response carries x-pnlol-open-items and x-pnlol-ready. This is a completeness check on our data, not a judgement about anyone's return.
Review queue and overrides (engine/src/overrides.js; on the dashboard)
reviewItems() lists what the engine cannot know (missing basis, transfers out, SOL basis, unpriced legs, unknown transactions), largest tax impact first; applyOverrides() applies a user's answers (cost basis, SOL basis, transfer out was a sale or a gift, ignore a spam token, set a price) to the event list before the FIFO run. Details: docs/OVERRIDES.md.
Known limits (v0)
- Free tool: last 500 wallet signatures only (a pass: up to 3,000 for now); older lots appear as
missing_basis. - Up to 3 wallets free / 10 with a pass; transfers to/from exchanges or other people still look like transfers out/in.
- No LP positions, staking, lending, perps, NFTs, or bridging. Those transactions show up as transfers or
unknown. - Not handled: wash sales (not law for tokens as of 2026-10-31; H.R. 10357 pending), specific identification, airdrop income.
- Floating-point JS numbers; token raw amounts are exact strings on events.