---
title: "FIFO cost-basis PnL on Robinhood Chain (PRO+)"
method: GET
path: "/rhc/wallet/{address}/pnl"
tags: ["Robinhood Chain"]
---

# FIFO cost-basis PnL on Robinhood Chain (PRO+)

`GET /rhc/wallet/{address}/pnl`

## Request

- Base URL: `https://madeonsol.com/api/v1`
- URL: `https://madeonsol.com/api/v1/rhc/wallet/{address}/pnl`
- Auth: HTTP bearer

Full FIFO cost-basis PnL for one RHC wallet over 90 days: realized and unrealized split, a daily realized curve, every closed position with ROI and token-weighted hold time, and every open position marked to the current price.

This runs the **same FIFO implementation as the Solana `/wallet/{address}/pnl`** — the algorithm is imported, not reimplemented — with ETH substituted for SOL. Field names keep the Solana convention with the denomination swapped (`realized_eth`, `pnl_eth`), so porting a Solana integration is a mechanical rename.

It also shares its loader with `/rhc/wallet/{address}`, so the two endpoints cannot report different PnL for the same wallet.

**Cost basis is only observable inside the window** (`notes.cost_basis_observable_from`). Tokens sold that were bought before it contribute no realized PnL rather than a fabricated one — figures read low, never invented.

**Attribution floor (`notes.attribution_complete_from`, 2026-07-18):** the window is clamped forward to this date because `trader_eoa` — the column every wallet endpoint filters on — was written for under half of earlier trades. The gap is ASYMMETRIC per wallet: a token's buys can carry it while its sell does not, which leaves FIFO holding a position that was actually closed. Analysing only the attributed era is narrower but honest. `notes.window_clamped` flags it. Cached 120s per address. Tier: **PRO+**.

## Path parameters

- `address` string, required

## Response `200`

OK.

- object
  - `chain` 'robinhood'
  - `address` string
  - `window_days` integer
  - `summary` object
    - `realized_eth` number
    - `unrealized_eth` number
    - `total_pnl_eth` number
    - `total_bought_eth` number
    - `total_sold_eth` number
    - `wins` integer
    - `losses` integer
    - `win_rate` number, nullable: Fraction in [0,1] across CLOSED positions.
    - `profit_factor` number, nullable: Gross wins / gross losses. null when there are no losses (undefined math), 0 when there are no wins.
    - `avg_hold_minutes` integer, nullable
    - `median_hold_minutes` integer, nullable
    - `max_drawdown_eth` number: Magnitude — always ≥ 0.
    - `open_positions_count` integer
    - `closed_positions_count` integer
    - `total_tokens_traded` integer
    - `best_realized` object, nullable
    - `worst_realized` object, nullable
  - `pnl_curve` object[]: Sparse daily UTC buckets of REALIZED PnL. `cumulative_pnl` is the running sum of `day_pnl`; days with no realized events are omitted.
    - `date` string
    - `day_pnl` number
    - `cumulative_pnl` number
    - `trades` integer
  - `closed_positions` object[]
    - `token_address` string
    - `token_symbol` string, nullable
    - `buy_count` integer
    - `sell_count` integer
    - `bought_eth` number
    - `sold_eth` number
    - `pnl_eth` number
    - `roi_pct` number, nullable
    - `hold_minutes` integer, nullable: Token-weighted across matched FIFO lots — correct for re-entered tokens.
    - `result` 'win' | 'loss' | 'breakeven'
    - `first_trade` string, date-time, nullable
    - `last_trade` string, date-time, nullable
  - `open_positions` object[]: Unsold FIFO tail marked to the current price. Each entry carries `liquidity_basis` (`v4_virtual_ceiling` | `measured`) — uniswap-v4 liquidity_usd is a virtual-reserve ceiling, not measured per-pool TVL.
  - `notes` object
    - `denomination` 'ETH'
    - `cost_basis_observable_from` string, date-time
    - `data_through` string, date-time, nullable: Newest trade this answer was computed from — the response's vintage. Equals `stats.last_seen` on /rhc/wallet/{address} when both ran over the same data. Use this, not a trade count, to tell whether two responses are comparable: the count saturates at the analysis cap.
    - `trades_seen` integer
    - `trades_analyzed` integer
    - `trades_unattributed` integer: No token side / direction (exotic or unresolved pool). Routine.
    - `trades_unsized` integer: Token-side trades with no reconstructable quantity. Should be ~0.
    - `partial` boolean
    - `partial_reason` string
  - `cache_hit` boolean

## Other responses

- `400`: Invalid EVM address.
- `401`: Missing or invalid API key, or an expired free-tier key. Authentication-stage error: no `X-RateLimit-*` headers, no `_rid`.
- `403`: Requires Pro or Ultra tier.
- `404`: Wallet has no Robinhood Chain trades in the data window.
- `422`: Trades exist but none carry a reconstructable token amount, so no PnL can be computed. Returning zeroes would assert the wallet is flat, which is a different claim.
- `429`: Too many requests. Every 429 carries `Retry-After`. A quota 429 (`rate_limit_exceeded`, with `X-RateLimit-*`), a heavy-query concurrency 429 (`error_kind` `concurrency`/`capacity`), or the per-IP authentication-failure block (30 failed auths/min; plain `error` + `retry_after_seconds`, no `X-RateLimit-*`).
- `500`: Unexpected server error from the route (`Internal server error`, with `_rid`). Safe to retry with backoff. A failed API-key lookup is a 503 `auth_backend_unavailable`, never a 500.
- `503`: Temporarily unavailable — a Postgres statement timeout while the dataset is under load or maintenance (`statement_timeout`, with `X-RateLimit-*` and `_rid`), or the API-key lookup itself unavailable (`auth_backend_unavailable`, authentication stage: no `X-RateLimit-*`, no `_rid`, not counted as a failed authentication). Always carries `Retry-After`; honour it.

---

[API](https://skmtc.dev/madeonsol/apis/madeonsol-robinhood-chain-api.md) · [All operations](https://skmtc.dev/madeonsol/apis/madeonsol-robinhood-chain-api/llms.txt) · [OpenAPI document](https://skmtc.dev/madeonsol/apis/madeonsol-robinhood-chain-api/revisions/9ea9a35ee2bf?raw)
