# texttcg agent API

Manage a texttcg card collection over plain HTTP. Auth: send a personal API
token (the user creates one at https://texttcg.com/settings?tab=agent) as
`Authorization: Bearer ttcg_...` on every request. All operations act on the
token owner's collection only.

## Read

- `GET /api/agent` — these docs.
- `GET /api/agent/cards` — the collection as JSON. Optional query params:
  `game` (pokemon|onepiece|vibes|riftbound|yugioh|naruto|sports|other), `status` (owned|sold|all,
  default owned), `q` (name search), `tag`. Each card: `id`, `game`, `status`,
  `name`, `set`, `rarity`, `acquired`, `cost_basis`, `asking_price` +
  `asking_price_at` (the owner's own asking price and when it was set),
  `sold_price`, `grade`, `tags`, `starred` (pinned to the top of the portfolio),
  `split_with` + `split_percent` (ownership split — who the card is co-owned
  with and their percent share),
  and the owner's consignment bookkeeping: `consignor` (whose card this really
  is, when they're selling it for someone), `consignor_split` (the consignor's
  percent of the sale), `consignment_settled_at` (when the payout was made).
- `GET /api/portfolio/export` — the whole collection as CSV.
- `GET /api/catalog/search?q=charizard` — search the card catalogs (all games);
  returns `hits` with `game`, `id` (catalog id), `name`, `set`, `rarity`,
  `detail`. Set names and rarity shorthand in `q` narrow results
  ("charizard 151 sir"). Optional `limit` (per game, max 100, default 6).
  Optional scope params: `game` (onepiece|pokemon|vibes|riftbound|yugioh|naruto) restricts
  to one catalog; `number` is a printed/collector number in that game's format
  (One Piece `OP01-001` → exact printings, Pokémon `4/102` → local id + set
  size, Riftbound number text); `rarity` filters that game's rarity labels
  (Pokémon shorthand like `SIR`/`full art`, One Piece `SR`/`SEC`, …).

Agent access requires an active texttcg subscription (either tier) — without
one, tokens return 401 until the subscription resumes.

## Write

- `POST /api/cards` — add a card. JSON body, one of:
  - `{"text": "phantasmal flames charizard sir", "price": 42, "tags": ["grail"], "acquired": "2026-07-01"}`
    — free text resolves like texting does (printed numbers, names,
    set + rarity, community nicknames). 422 = no catalog match.
  - `{"game": "pokemon", "catalogId": "sv03.5-205", "price": 12}` — exact printing.
  - `{"customName": "1996 Topps Chrome Kobe Bryant #138", "customSet": "1996 Topps Chrome", "customRarity": "Refractor", "game": "sports", "price": 500}` —
    catalog-less entry (sports cards, sets the catalogs don't carry).
    `customSet` is optional and becomes the entry's set in the portfolio;
    `customRarity` is optional and becomes its rarity/parallel (e.g.
    "red white blue"), shown wherever catalog rarities are.
    `game` says which portfolio tab the entry belongs to — `sports`, one of the four
    catalog games, or `other` (the default when omitted). Changeable later via
    `PATCH /api/cards/:id` `{"game": "sports"}` (custom entries only).
  All optional: `price` (number, cost basis), `tags` (string[]),
  `acquired` (YYYY-MM-DD), `splitWith` (ownership split — who the card is
  co-owned with) with an optional `splitPercent` (their percent share, 0–100),
  and — a subscriber feature — `consignor` (whose card this is,
  when you're adding one you're selling for someone) with an optional
  `consignorSplit` (their percent of the sale, 0–100). Returns `{ok, cardId, name}`.
- `PATCH /api/cards/:id` — edit. One key per request:
  `{"prices": {"cost_basis": 20}}` · `{"prices": {"sold_price": 50}}` ·
  `{"prices": {"asking_price": 75}}` (the owner's asking price — their vendor tag;
  timestamps itself on every change, null clears) ·
  `{"tags": ["grail","vault"]}` · `{"notes": "pulled at locals"}` ·
  `{"acquired": "2026-06-15"}` · `{"starred": true}` (pin to top) ·
  `{"grading": {"company": "PSA", "grade": "10"}}` or `{"grading": null}` ·
  `{"split": {"with": "kyle", "percent": 50}}` (ownership split — the card is
  co-owned; `percent` is the partner's share, optional) ·
  `{"split": null}` (clear it) ·
  `{"consignment": {"consignor": "kyle", "split": 70}}` (subscriber — track a
  card sold on someone's behalf; `split` is their percent of the sale, optional) ·
  `{"consignment": {"settled": true}}` (the payout was made) ·
  `{"consignment": null}` (clear it). The owner's ledger renders at /consignment.
- `POST /api/cards/:id/sell` — mark sold: `{"price": 50}` (price optional).
Market estimates everywhere (alerts, wants) come from the US market (recent
sales of ungraded raw copies, USD). The account's estimate-source setting
(Settings → Pricing, `price_source`: auto|us) selects it; both currently
resolve to the US market.

- `GET /api/watchlist` — the pricing watchlist (/watchlist; adding is a
  subscriber feature — either tier): exact printings
  from any of the four games. Each row: the US-market raw-copy spread
  (market/low/mid/high, USD) plus self-computed history stats (7d/30d
  averages, PSA 10 latest + averages) that fill in as daily snapshots
  accumulate. `POST /api/watchlist` `{"game": "pokemon", "catalogId":
  "sv03.5-199"}` adds a printing (catalog-matched only — no free text);
  `{"fromInventory": true}` pulls in every matched owned card;
  `{"cardIds": ["..."]}` adds specific portfolio cards (matched only);
  `DELETE {"id"}` removes; `POST /api/watchlist/refresh` `{"id"}` asks the
  team to refresh a stale card's pricing (one request per card per day).
  Pricing in the GET is null until the user accepts the market-estimates
  disclaimer.
- `GET /api/decks` — the deck builder (/decks): the user's decks, each with
  its card count, whether it's shared (`isPublic`), and its share slug
  (`shareSlug`, null until the deck's first publish). Decks are auto-named
  from their slot (`deck1`..`deck6` — renaming is a planned follow-up) and
  capped at six per game; Pokémon, One Piece, and Riftbound for now.
  `POST /api/decks` `{"game": "pokemon"|"onepiece"|"riftbound"}` creates a
  deck in the lowest free slot (a subscriber feature — either tier);
  `DELETE {"id"}` removes one. `GET /api/decks/:id` — one deck's sharing
  state (`public`, `shareSlug`, `shareUrl`). `PATCH /api/decks/:id`
  `{"public": true}` publishes the decklist read-only at `shareUrl`
  (/d/:shareSlug) for anyone holding the link — it mints the slug on the first
  publish and reuses it afterward; `{"public": false}` takes the page down
  (the link stays reserved, so republishing restores it). Decks are private
  until this is called. Publishing is a subscriber feature; unpublishing is
  never gated. `GET /api/decks/:id/cards` — the
  decklist plus construction checks (`checks.items` is the checklist,
  `checks.ready` says tournament-shaped); `POST /api/decks/:id/cards`
  `{"catalogId": "sv03.5-199", "qty": 4}` sets a printing's quantity
  (absolute; 0 removes; exact catalog printings only, from
  /api/catalog/search). Enforced on write — Pokémon: the 60-card ceiling and
  max 4 copies per name across printings (alternate arts count; basic Energy
  exempt). One Piece: one Leader at qty 1, the 50-card main-deck and 10-card
  DON!! ceilings, max 4 copies per card number (alternate arts share it;
  DON!! exempt), and every main-deck card must share a color with the Leader
  (dual-color Leaders permit either color). Riftbound: one Legend, at least 1
  Champion, a main deck of at least 40 cards, exactly 3 Battlefields, max 3
  copies per card number (Legend/Runes exempt), and every card must share a
  domain with the Legend (dual-domain Legends permit either; the Rune deck is
  recommended at 12 but never required). Completeness (exact sizes, a Basic
  Pokémon, a Leader/Legend picked) is reported, never blocked — half-built
  decks save fine. `DELETE {"id"}` (a decklist row id) removes a row.
- `GET /api/alerts` — the user's price alerts (/scanner) with current estimates.
  `POST /api/alerts` `{"catalogId": "sv03.5-199", "direction": "above", "threshold": 500}`
  arms one (Pokémon only; a Power User feature; requires the user's
  market-estimates consent — the API says so if either is missing). `PATCH {"id"}` re-arms a fired alert; `DELETE {"id"}` removes.
  Fired alerts text the user once.
- `GET /api/wants` — the shopping list (cards the user wants but doesn't own).
  `POST /api/wants` with `{"name": "...", "resolve": true}` (resolve against the
  catalogs) or `{"name": "..."}` (kept exactly as typed) or `{"game", "catalogId"}`
  (exact printing); optional `targetPrice` (USD — Pokémon wants text the user when
  the estimate dips under it) and `note`. `PATCH {"id", "fulfilled": true}` marks
  it landed; `DELETE {"id"}` removes.
- `DELETE /api/cards/:id` — remove an entry (photo purged too).

## Notes

- Prices are plain numbers in USD. Portfolio history is append-only: selling
  flips status, never deletes.
- Rate-limit yourself politely; this is a small product.
- MCP option for Claude:
  `claude mcp add --transport http texttcg https://texttcg.com/api/mcp --header "Authorization: Bearer ttcg_..."`
