# Pullbook (seller ledger)

Pullbook is a profit ledger for people who sell Pokémon cards. Log what you buy (a sealed product, a lot, singles or a spot in a break), add what came out of it, and Pullbook splits the cost across the cards by market value, to the cent. Use it on the web at [https://raritydb.com/pullbook](https://raritydb.com/pullbook), or through the API, MCP and CLI below.

## How cost is split

- A purchase's **total cost** is its price + shipping + tax, in your ledger currency (USD unless you change it before your first purchase).
- Each item gets a share in proportion to its **weight**: a card's Cardmarket trend price (only the ratios matter, so euro prices weigh a dollar ledger fine), a bulk card's **bulk weight** (default 0.05, so twenty bulk cards weigh as much as a €1 card), or a sealed product's expected value (or MSRP).
- A card with no market price weighs as much as the median item in its purchase. Set your own value with `valueOverride` (per unit, on the € scale) when you know better.
- **Opening** a sealed item makes it a container: its share passes to the cards that came out of it. A sealed item with several units opens one at a time.
- **Extra costs** such as grading belong to the item alone (`extraCost`) and aren't split. The item's **cost basis** is its cost plus its extra cost.
- Sold items keep the cost they sold with, even if you edit the purchase later.

## Holdings: what it's worth today

**Holdings** shows everything you still hold (not sold, not opened) at market value, beside what you paid and the gain, split into sealed and singles, with a chart of the last 90 days.

- **Market value** is Cardmarket's trend price (its average where there's no trend) times how many you hold, converted from euros into your ledger currency at the European Central Bank's rate for that price's date ([Prices in US dollars](/docs/prices#prices-in-us-dollars)). It's an estimate of the market, not an offer: selling costs fees and shipping, and US prices can differ. Bulk and products Cardmarket doesn't list have no market value and are left out of it.
- **Cost unknown.** For cards you've had for years, log the purchase with "I don't know what I paid" (`costUnknown: true`; CLI `--cost-unknown`) instead of a price. Its amounts are 0, its items count toward market value but never toward gain, and a sale from it says "Unknown" for cost basis and gain in the tax export. In profit reports a sale with no known cost counts its whole net as profit.
- The chart values what you hold today on each past day, at that day's price and rate.

## Sales and profit

- A **sale** is one order: what the buyer paid (`gross` plus `shippingCharged`), what the marketplace kept (`fees`) and what you paid to ship it (`shippingCost`). `net` = gross + shipping charged − fees − shipping cost.
- **Fees** come from the channel's schedule unless you enter them. Whatnot: 8% of the item price + 2.9% of price and shipping + $0.30. eBay: 13.25% of price and shipping + $0.30 ($0.40 over $10). TCGplayer: 10.75% of the item price + 2.5% of price and shipping + $0.30. In person and other: none. Each sale keeps the fee it was charged, so schedule changes never rewrite history.
- Each **line** names what sold: an inventory item, a card (your oldest unsold copy bought on or before the sale is used, first in first out) or just a title. Selling part of a bulk row splits it. The item's cost basis is frozen on the line, and its profit is its share of the net minus that cost.
- A line with nothing to match waits in **review**: match it to an item, or say it wasn't from your ledger and what it cost.
- **Profit** counts settled sales: a sale with a line still in review is left out of profit until it's settled.
- **Streams and shows** group sales; an event's profit is its settled sales' net, minus their cost, minus what the event cost (a table, a booth).
- **Card-show mode** ([https://raritydb.com/pullbook/counter](https://raritydb.com/pullbook/counter)) is one form for the table: type the card and the price. It sells your oldest raw copy, asks which when the match could be different things, and can record something it can't find so you match it later.
- Selling a card by name or code never picks a graded copy; sell a slab by choosing that item.
- Free accounts record 25 sales a month (counted when you record them).

## Importing sales

- Upload a marketplace's sales export (CSV, up to 2 MB and 5,000 rows) at [https://raritydb.com/pullbook/imports/new](https://raritydb.com/pullbook/imports/new). Whatnot, eBay and TCGplayer columns are found by their names; for any other file you say which column is the date, the item and the price (and optionally the order number, SKU, set, card number, quantity, shipping, fees, buyer and stream). The mapping is remembered for the next file with the same columns.
- Rows with the same order number make one sale. Fees come from the file when it has fee columns (several are added up), otherwise from the channel's schedule. A stream or show column becomes an event.
- **Re-importing is safe**: an order number (or, without one, an identical row) is recorded once.
- A card code in the SKU or title (`sv03.5-006`), or the set and card number, matches the card and sells your oldest raw copy. A name alone is a guess, so the line waits in **review** at [https://raritydb.com/pullbook/review](https://raritydb.com/pullbook/review): confirm a copy, search for the right card, or say it wasn't from your ledger.
- On the free plan an import stops at the month's 25 sales and reports how many orders were left out.
- Rows with a negative price look like refunds; they're reported, not imported, so adjust that sale by hand.

## Reports and taxes

- [Reports](https://raritydb.com/pullbook/reports) show what you made by **purchase** (what it cost, what sold from it, what's left), by **stream or show** (minus what it cost), by **product** and by **month** (minus that month's stream and show costs), for one year or all time.
- Profit is what buyers paid after fees and shipping, minus what the items that sold cost you. A sale with an item still in review isn't counted until it's matched.
- The **tax export** (paid plans) is a CSV of every item you sold in a year: what it was, when you bought and sold it, proceeds, cost basis, gain or loss, and whether you held it more than a year. It's a summary of your records, not tax advice: collectibles can be taxed differently, so check it with a tax professional.

## GraphQL

```graphql
mutation {
  createPurchase(kind: SEALED, sealedProductId: "<SealedProduct id>", quantity: 2, purchasedOn: "2026-10-01", price: 50, shipping: 4) {
    id totalCost items { id kind label cost }
  }
}
```

```graphql
mutation {
  addPulls(purchaseId: "<purchase id>", fromItemId: "<sealed item id>", bulk: 40,
    pulls: [{ cardCode: "sv03.5-006", printingKey: "holo" }]) {
    items { id label cost openedFromId }
  }
}
```

```graphql
{ viewer { pullbook { currency inventory { label quantity costBasis } } } }
```

```graphql
{ viewer { pullbook { holdings { currency value paid gain gainPercent costUnknownQuantity
  groups { kind value quantity } rows(kind: "singles") { value cost gain costUnknown item { label } } history { date value } } } } }
```

```graphql
mutation {
  createSale(channel: WHATNOT, soldOn: "2026-10-10", shippingCharged: 4, shippingCost: 4.5,
    lines: [{ cardCode: "sv03.5-006", price: 40 }]) {
    net profit lines { title status costBasis profit }
  }
}
```

Sales: `createSale`, `deleteSale` (its items go back in stock), `resolveSaleLine` (`itemId`, `costBasis`, `unmatch: true`, or `cardCode` to say which card a line in review is), `createEvent`, `updateEvent`, `deleteEvent`; read `viewer.pullbook { sales reviewQueue events salesThisMonth }`.

Reports: `viewer.pullbook { report(groupBy: PURCHASE, year: 2026) { rows { label net costBasis profit } total { profit } inReview } taxLots(year: 2026) { description acquiredOn soldOn proceeds costBasis gain term } }`.

Imports: `importSales(csv:, channel:, filename:, mapping: [{ field, headers }])` returns a `PullbookImport`. If its `status` is `needs_mapping`, its `headers` and `missingFields` say what to map; send that with `finishImport(id:, mapping:)`. Fields you send replace what was guessed; `headers: []` (CLI: `--map fees=`) unmaps one. Unknown fields or columns that aren't in the file are refused. The mapping an import used is reused for the next file with the same columns. Imports run in the background: read `viewer.pullbook { import(id:) }` until its `status` isn't `running` (the CLI waits for you). Past imports are in `viewer.pullbook { imports }`.

Also: `updatePurchase`, `deletePurchase` (not once something from it has sold), `openItem`, `updateItem` (value, condition, grade, extra cost), `deleteItem`, `addExtraCost` (spread one bill, like a grading submission, evenly over items) and `updatePullbookSettings`. Pullbook records are private: they're reachable only through `viewer.pullbook`, never `node(id:)`.

## Command line

```bash
raritydb pullbook buy --kind sealed --product "151 Booster Bundle" --quantity 2 --price 50 --shipping 4
raritydb pullbook pulls <purchase-id> sv03.5-006:holo sv03.5-025 --bulk 40 --from <sealed-item-id>
raritydb pullbook extra-cost <item-id> --amount 25 --note "PSA"
raritydb pullbook inventory
raritydb pullbook buy --kind lot --name "My childhood binder" --date 1999-12-25 --cost-unknown
raritydb pullbook holdings --kind sealed   # market value, what you paid, gain
raritydb pullbook sell sv03.5-006 40 --channel whatnot --shipping 4 --shipping-cost 4.50
raritydb pullbook review              # sold lines waiting for a match
raritydb pullbook resolve <line-id> --item <item-id>
raritydb pullbook import whatnot-orders.csv --channel whatnot
raritydb pullbook import sales.csv --channel other --map sold_on=When --map title=What --map item_price=Amount
raritydb pullbook resolve <line-id> --card sv03.5-025   # it was this card; then --item one of your copies
raritydb pullbook report --by event --year 2026
raritydb pullbook tax --year 2026 --csv > pullbook-sales-2026.csv
```

## From an agent (MCP)

The `pullbook_purchases` tool does all of the above with an `action`: `list`, `get`, `create`, `update`, `delete`, `add_pulls`, `open_item`, `update_item`, `delete_item`, `add_extra_cost`, `inventory` and `settings`. The `pullbook_sales` tool records and settles sales and manages streams and shows: `list`, `get`, `review`, `create`, `delete`, `resolve`, `import`, `finish_import`, `imports`, `events`, `create_event`, `update_event` and `delete_event`. The read-only `pullbook_report` tool returns reports (`report`, with `group_by` and `year`), `tax_lots` for a year and `holdings` (optional `kind`). Create a purchase with `cost_unknown: true` instead of a price when you don't know what you paid.

## Plans

Free accounts can record 25 sales a month. Pro ($12 a month or $99 a year) and Dealer ($29 a month or $249 a year) have no limit, and the first 50 sellers can lock in Founding at $79 a year. Paid plans also include the tax export; reports are free on every plan.

Buy a plan on [Plans](https://raritydb.com/pullbook/upgrade), and change or cancel it there under Manage billing. Payment happens on Stripe's page in a browser, so the API, MCP and CLI hand you the link:

- **GraphQL:** `createPullbookCheckout(plan: "pro", interval: "year")` and `pullbookBillingPortal` each return a URL.
- **MCP:** `pullbook_purchases` with `action: "checkout"` (`plan`, `interval`) or `action: "billing_portal"`.
- **CLI:** `raritydb pullbook upgrade pro --interval year` and `raritydb pullbook billing`.

Your plan changes once Stripe confirms the payment. If a renewal fails, Stripe retries it and you keep your plan meanwhile; if the subscription ends you move to Free and keep all your records.
