{"version":"2026-10-01.1","title":"Pokoin marketplace client contract for React / Flutter / JS","hosts":{"api":"https://api.pokoin.com","publicSite":"https://pokoin.com","cdn":"https://cdn.pokoin.com","rewriteNote":"https://pokoin.com/api/* rewrites to https://api.pokoin.com/api/*. Do not add Vercel serverless functions.","localDev":"npm run api:server; default http://127.0.0.1:18080","apiRuntime":"Docker pokoin-oracle-api on pi-home (Raspberry Pi), exposed through Cloudflare tunnel"},"identity":{"publicIdField":["id","card_id"],"formula":"For CardTrader-backed catalog records: public_card_id = cardtrader_blueprint_id * 2. Use the returned public id; do not apply this formula to arbitrary external/provider ids.","examples":{"espurr":{"card_id":"220962","ct_id":110481},"magcargo":{"card_id":"587148","ct_id":293574},"dachsbun":{"card_id":"598052","ct_id":299026},"mewExSirPaldeanFates":{"card_id":"548832","ct_id":274416},"megaLucarioEx":{"card_id":"703382","ct_id":351691}},"rules":["Path segments and cardId query params are always the public id.","Never divide a public id by 2 when calling the API.","Never multiply an id that already came from the API (that 4xs).","doubledCardId is only for raw leftover ct_id from Milo/CLIP (hit.id, hit.ct_id, blueprint_id).","Milo gallery identity is leftover ct_id (manifest identity=ct_id). Public card_id = ct_id * 2. TCGplayer product ids are not Milo ids.","ct_id may appear on JSON for joins/debug; never put it in the address bar."],"canonicalPath":"/marketplace/{lang}/cards/{card_id}/{slug}","lookup":"GET /api/marketplace-card-url?cardId={publicId}","sql":"018_pokoin_card_id_ct_id.sql","decision":"Codevira D00000B"},"images":{"r2KeyPrefix":"Legacy Pokemon objects use ct_id; satellite games may use game-specific prefixes. Treat keys as opaque and use API image URLs.","doNotBuildFilenamesFromPublicId":true,"fields":{"imageUrl":"Full art (typically 330x460 baseline JPEG). Prefer this for grids and heroes.","previewImageUrl":"180px tile. Do not stretch as a hero. API may still return /previews/*.jpg.","homepageImageUrl":"Only use when it contains _homepage.webp. Often wrongly equals preview for new cards.","gridImageUrl":"React grid: always the full raster, never /previews/.","heroImageUrl":"React detail/hero: always the full raster.","tileImageUrl":"React compact tile: _homepage.webp when present, else full raster. Never preview JPEG."},"cdnWorker":"pokoin-cdn-card-images","r2Bucket":"cardvault-images","observability":"GET/POST /api/marketplace-image-log","decodeNote":"Flutter CanvasKit fails progressive JPEG and tiny VP8 preview webp. React <img> can decode those, but still must not use 180px previews as full-size art.","rules":["Use gridImageUrl for grids and heroImageUrl for detail.","Do not synthesize image keys from public IDs. Palworld/Cyberpunk legacy image keys use raw CardTrader IDs; the API handles this mapping."]},"availability":{"serverOwnsFlag":true,"fields":{"isMarketAvailable":"boolean — show price vs Out of stock","inStock":"boolean — same value, HTML-friendly alias","stock":"int — native + eligible CardTrader quantity","hasCardTraderListing":"bool","cardtraderEligibleListingCount":"int","price":"number PKN ask when available"},"formula":"stock > 0 || hasCardTraderListing || cardtraderEligibleListingCount > 0","cheapestCacheJoin":"blueprint_id = ct_id OR pokoin_card_id = card_id::text. Never blueprint_id = public card_id.","emptyCacheIsData":"SV commons can be genuine OOS if cheapest_homepage_cache_blueprint has no row."},"search":{"engine":"meili","scope":"Meilisearch for English Pokemon retrieval; SQL for satellite games and legacy non-English full search","host":"127.0.0.1:7700 on pi-home alongside the public API","indexes":["marketplace_cards","marketplace_name_tokens"],"queryParam":"query","queryAlias":"q","notes":["Pokemon suggest retrieves grouped printings from Meilisearch; optional SQL enrichment supplies title language and expansion nationality.","Satellite-game suggest uses the selected game catalog SQL path.","Non-English Pokemon suggest returns an empty result with meta.reason=meili_unavailable when the language gate is closed; do not assume it uses full-search SQL fallback.","Search page hydrates identity/image and overlays cached cheapest PKN. Shop listing rows remain on the card page.","POST /api/marketplace-autocomplete remains the Flutter pool endpoint; POST /api/searchbar-token-predict supplies ghost-text prediction.","Plain prefixes favor the base display name; explicit variant tokens retain variant intent.","Meilisearch stays on pi-home. Oracle is the catalog import/write primary."],"params":{"query":"Name / number string","limit":"Page size (search path caps ~100)","offset":"Meili offset","search_language":"Display/search language; aliases lang and language. Default en.","productType":"Facet","productSearchOnly":"1 = Product search universe: sealed products plus the jumbo subtype (no separate jumbo tab)","game":"Game scope; see games.supported","print_language":"Print universe all|western|japanese|korean|chinese; alias printLanguage. Japanese and Korean selections share the JP/KO universe.","match":"Suggest only: all requires every query token; omitted uses the default matching strategy."},"autocomplete":"POST /api/marketplace-autocomplete { search_term, result_limit } (Flutter)","suggest":"GET /api/marketplace-suggest?q= (pokoin-web popup)","fullSearch":"GET /api/marketplace-cards?query=","searchPage":"GET /api/marketplace-search-page?query=","tokenPredict":"POST /api/searchbar-token-predict","counts":{"suggest":"shown is the capped number of popup printings (at most 20). count is the matching/filtered universe estimate, not the popup length. globalCount is available on Pokemon responses.","searchPage":"count is the returned page length. total is the matching query total when available, otherwise null; never treat count as the catalog total. Use hasMore and offset for pagination."}},"auth":{"provider":"Firebase Auth","header":"Authorization: Bearer <Firebase ID token>","login":"POST /api/auth-login","logout":"Client-side Firebase signOut. No server logout.","extensionBridge":"GET /extension/auth-bridge","rules":["Protected routes derive UID from the verified Firebase token, never from a client body UID.","Do not log or embed bearer tokens in documentation. See routes[].auth for each endpoint."]},"pageApis":{"home":{"path":"GET /api/marketplace-home-page","legacy":"GET /api/marketplace-home is the Flutter snapshot (CardTrader hydrate, often 30s+). React must not call it.","query":"game; recentCardIds optional comma-separated public ids; limit default 36, max 48","returns":"game, cards[], sections.{recentlySeenIds,bestSellerIds,featuredIds,newArrivalIds,spotlightIds}; optional source","cache":"With recentCardIds: private, no-store. Otherwise public max-age=15, s-maxage=30, stale-while-revalidate=60."},"suggest":{"path":"GET /api/marketplace-suggest","query":"q|query, game, limit (default 20, max 24 groups; popup capped at 20 printings), search_language|lang|language, print_language|printLanguage, match=all optional","returns":"query, game, groups[] of {name, printings[{id,set,number,rarity,image,href,...}]}, shown, count; Pokemon also globalCount and printLanguage. Empty/degraded responses can include meta.reason.","cache":"public max-age=5, s-maxage=30, stale-while-revalidate=120 on successful reads; degraded responses use shorter caching."},"search":{"path":"GET /api/marketplace-search-page","query":"query|q, game, limit (default/max 100), offset (default 0), productType, productSearchOnly=1, search_language|lang|language, print_language|printLanguage, includeFacets (default enabled; 0 disables)","returns":"query, game, productType, productSearchOnly, lang, cards[], facets.products[], count, total (nullable), hasMore, limit, offset. Identity/image hydrate plus cached cheapest PKN; no live shop-listing read.","cache":"public max-age=15, s-maxage=60, stale-while-revalidate=120"},"card":{"path":"GET /api/marketplace-card-page","query":"cardId|id required (public id), game, lang|language|search_language, cardSlug|slug, includeSales, includeOffers, includeSameAs, liveOffers (all flags default off; 1|true|yes enables), offerLimit (default 40, max 80), salesLimit (default 40, max 120)","returns":"card, game, version, visualTheme, versionCount, versions[], rarities[], neighbors.{prev,next}, sameAs[], offers[], cheapest, sales[], artist, canonicalPath, seo, lookup","offers":"includeOffers=1 is required to populate offers. liveOffers=1 additionally requests live CardTrader; otherwise only native listings are requested. Optional reads may return empty arrays on timeout/failure.","cache":"public max-age=10, s-maxage=30, stale-while-revalidate=60"},"expansion":{"path":"GET /api/marketplace-expansion-page","query":"game; expansionName or slug; productType default card; limit (default 200, max 400), offset. Omit both name and slug for the expansion index (limit default 500, max 2000).","returns":"expansion { name, slug, cardCount }, cards[], total, count, hasMore, limit, offset. cardCount/total is stored catalog_card_count (grid singles with art), not the page size and not TCGDex printedTotal.","cardCount":"Read public.marketplace_set_card_counts.catalog_card_count / pokoin_pokemon_expansions.catalog_card_count. Refresh: refresh_marketplace_set_catalog_counts(). Do not COUNT(*) on the request."},"portfolio":{"path":"GET /api/marketplace-portfolio","query":"game; id optional; limit Pokemon default 400/max 500, satellite default 2000/max 2500","returns":"game, currency, totals, games[], items[], holdings[]. Catalog explore data, not authenticated personal holdings.","pricing":"PKN, with available native-listing and Pokemon cheapest-cache overlays; unpriced rows use pricePkn=0."}},"silver":{"cardtrader":"SPA opens leftover https://www.cardtrader.com/en/cards/{ct_id} with window.open noopener,noreferrer (no Pokoin referrer, not a Google cloak). Lookup fallback GET /api/cardtrader-redirect?id={publicId}&format=json. Sanji 818358 → 409179. Never send public ids to cardtrader.com.","cardmarket":"GET /api/cardmarket-redirect?id={publicId}&format=json. Pokemon: stored/product URL, else Singles search {name token} {collector} (dawn 129), not set name. OP: /en/OnePiece/Products/Search {name} {number}. RB: /en/Riftbound/Products/Search. Datacenter IPs often get Cloudflare 403 on cardmarket.com — the user browser opens the URL.","vinted":"SPA builds https://www.vinted.it/catalog?search_text=. Pokemon: {name} {collector hash} (Gumshoos 184 from 184/182), not name alone and not English set name (Vinted ANDs tokens). OP: One Piece Card Game {name} {number}. RB: Riftbound TCG {name} {number}."},"canonicalCardJson":{"id":"548832","card_id":"548832","ct_id":274416,"name":"Mew ex","set":"Paldean Fates","number":"232/091","rarity":"Special Illustration Rare","itemKind":"single","productType":"card","canonicalPath":"/marketplace/en/cards/548832/card-mew-ex-special-illustration-rare-232-091-paldean-fates","imageUrl":"/card-images/{card_id}_….jpg","previewImageUrl":"/card-images/{card_id}_….jpg","gridImageUrl":"/card-images/{card_id}_….jpg","heroImageUrl":"/card-images/{card_id}_….jpg","isMarketAvailable":false,"inStock":false,"acceptBothCamelAndSnake":true},"forbiddenInReact":["Halve path ids or double catalog ids","Concatenate card.id + '_' + slug for CDN keys","Prefer /previews/*.webp or /previews/*.jpg because they are smaller","Use homepageImageUrl when it is a /previews/ URL","Recompute isMarketAvailable from partial fields","ILIKE search in the browser","Join listings on public id instead of ct_id","Guess slugs instead of canonicalPath","Hardcode leftover CardTrader ids (Mew 274416 is Shining Kabutops)","Trust client-calculated PKN for payments","Add Vercel serverless marketplace handlers","Send public card_id to cardtrader.com (use leftover ct_id via cardtrader-redirect)"],"navigation":{"contract":"GET /api/__contract","routes":"GET /api/__routes","routesGrouped":"GET /api/__routes?group=1","routesByFamily":"GET /api/__routes?family=page-bff","families":["page-bff","search","card","catalog","commerce","scan","cardtrader","cardmarket","auth","payments","assistant","social","debug","other"],"doNotMoveApiFiles":true,"docs":["docs/react-api-architecture.md","docs/react-page-apis.md","docs/oracle-api-migration.md","docs/api-route-catalog.json","docs/pokoin-api.md","workflows/meilisearch-peer-workflow.md","memory/architecture.md"]},"bffGaps":[],"stranglerOrder":["home","search","card-detail","expansion"],"flutterNativeStays":true,"doNotRewriteWholeFlutterRepo":true,"routeCount":147,"updatedAt":"2026-10-01","scope":"Client API reference. Publication of this contract does not deploy business-logic changes. routes and routeCount are derived from the running server manifest.","topology":{"api":"pi-home","meilisearch":"pi-home, http://127.0.0.1:7700","cache":"Valkey on pi-home","pokemonCatalogReads":"Postgres streaming replica on pi-home","catalogPrimary":"pokoin-marketplace Oracle: CardTrader imports and migrations write to the primary, never to the Pi replica","satelliteCatalogs":"Game-scoped isolated databases; selecting a game does not create an isolated user-listings store."},"games":{"default":"pokemon","supported":["pokemon","magic","yugioh","flesh_and_blood","digimon","dragon_ball_super","vanguard","one_piece","lorcana","star_wars","union_arena","riftbound","gundam","sorcery","palworld","cyberpunk"],"query":"game (alias marketplaceGame)","headers":["x-pokoin-game","x-marketplace-game"],"resolution":"Non-Pokemon query scope first, then game header, then recognized forwarded/site hostname. Unknown game values normalize to pokemon.","rule":"Carry the selected game across search, detail, expansion, portfolio and listing requests. Public IDs returned by the API are already normalized."},"listings":{"path":"/api/marketplace-listings","methods":["GET","POST","PATCH"],"publicRead":"GET with cardId (public id), game and limit (default 500, max 1000). nativeOnly=1 or live=0 skips live CardTrader. Response: {listings:[]}.","sellerRead":"sellerUid requires a Firebase bearer token matching the owner UID. sellerUsername reads active public seller inventory. Do not pass both.","write":"POST creates; PATCH?id=<listing-id> updates. Firebase bearer required; ownership and reserve privileges are checked server-side.","gameScope":"Native marketplace_user_listings is the shared central store; catalog/game tags scope seller inventory. Clients must send game and must not choose a database.","cache":"private, no-store"},"payments":{"currency":"PKN for marketplace values","authority":"Prices, balances, ownership and privileges are checked by the server. Client totals are not payment authority.","discovery":"See routes for marketplace-checkout-quote, create-order-checkout-session, marketplace-orders, create-pkn-checkout-session and stripe-webhook; availability is determined by the running manifest."},"health":{"path":"GET /healthz (alias /api/healthz)","dependencies":["Postgres","Valkey","Meilisearch","CDN"],"unhealthyStatus":503},"errors":{"pipelinePolicy":{"status":503,"body":{"error":"We are working on a solution."}},"handlerErrors":"Validation, auth and not-found errors retain their handler HTTP status. Some handlers still return 500 errors or degraded 200 payloads; do not assume every failure is normalized to the pipeline policy.","suggest":"Empty/degraded suggestions may include meta.reason; an empty popup alone is not proof that no catalog matches exist."},"compatibility":{"flutterUsage":"docs/flutter-api-usage.json is a repository audit snapshot, not the running route count.","deploymentNote":"The inspected Pi release predates the Sep 30 central-listings/game-forwarding fixes in GitHub 8a88881. This documentation update does not install those business-logic patches; satellite card-page offers can therefore remain incomplete until that release is deployed."},"routes":[{"path":"/api/auth-login","methods":["POST","OPTIONS"],"family":"auth","purpose":"Validate the current Firebase bearer token and return safe auth metadata.","auth":"Required Firebase bearer token.","params":{"body":"No required JSON body fields."}},{"path":"/api/chat","methods":["GET","POST"],"family":"other","purpose":"List and read authenticated direct conversations, send messages, and create atomic PKN payment events.","auth":"Required Firebase bearer token; conversation membership is rechecked for every action.","params":{"query":"`action` is `list`, `get`, `message`, `pay`, or `read`; `peer` is used by GET conversation reads.","body":"`peer` plus `text`, `amountPkn`, or read/payment fields according to action."}},{"path":"/api/cache-google-profile-picture","methods":["POST"],"family":"auth","purpose":"Download the authenticated user Google avatar, optimize it, and store it in R2.","auth":"Required Firebase bearer token.","params":{"body":"No required fields; uses the authenticated Firebase profile picture URL."}},{"path":"/api/cardmarket-redirect","methods":["GET"],"family":"cardmarket","purpose":"Resolve a marketplace blueprint to a Cardmarket product/search URL and redirect, or return JSON when requested.","auth":"Public.","params":{"query":"`id` public card_id or leftover ct_id, optional `blueprintId` leftover, `locale` optional two-letter locale, `format=json` optional, `game` for One Piece / Riftbound."}},{"path":"/api/cardmarket-scrape-observation","methods":["POST","OPTIONS"],"family":"cardmarket","purpose":"Record Cardmarket scrape/association observations used by marketplace import review tooling.","auth":"Required Firebase bearer token.","params":{"body":"Cardmarket observation payload including blueprint/product identifiers and scrape metadata."}},{"path":"/api/cardtrader-blueprint-listings","methods":["GET","OPTIONS"],"family":"cardtrader","purpose":"Return historical/daily CardTrader marketplace listing snapshots for one blueprint/card ID from Oracle.","auth":"Public.","params":{"query":"`blueprintId` required for CardTrader blueprint IDs; `cardId` accepted for mapped Pokoin card IDs; `limit`, `page`, and `cursor` optional."}},{"path":"/api/cardtrader-live-listings","methods":["GET","OPTIONS"],"family":"cardtrader","purpose":"Return live on-demand CardTrader marketplace listings for one blueprint/card ID without persisting results.","auth":"Public.","params":{"query":"`blueprintId` for a CardTrader blueprint ID or `cardId` for a Pokoin card ID such as `248856`; `language`/`lang` and `limit` optional. Without `limit`, returns all rows from CardTrader for the blueprint; explicit limits cap the client response only."}},{"path":"/api/cardtrader-clean-listings","methods":["POST"],"family":"cardtrader","purpose":"Deactivate CardTrader-linked listings owned by the authenticated seller.","auth":"Required Firebase bearer token for the seller.","params":{"body":"No required fields."}},{"path":"/api/cardtrader-connect","methods":["POST","DELETE"],"family":"cardtrader","purpose":"Connect, replace, or disconnect an authenticated seller CardTrader token.","auth":"Required Firebase bearer token.","params":{"body":"`token` required for POST. DELETE has no body."}},{"path":"/api/cardtrader-daily-listings-refresh","methods":["GET","POST"],"family":"cardtrader","purpose":"Manual/admin diagnostic trigger for global CardTrader marketplace listing snapshots; scheduled ingestion is owned by the Oracle/peer4 host script.","auth":"Required CARDTRADER_DAILY_LISTINGS_SECRET, CARDTRADER_DAILY_REFRESH_SECRET, or CRON_SECRET bearer/header secret.","params":{"query":"`dryRun`, `maxBlueprints`, `maxProducts`, `archiveMissing`, `removedDay`, `blueprintId`, `blueprintIds`, `expansionId`, and `language` are optional bounded controls.","body":"Same controls as query parameters for POST."}},{"path":"/api/cardtrader-game-ingest","methods":["GET","POST","OPTIONS"],"family":"cardtrader","purpose":"List isolated satellite-game ingest services, inspect one game, or run a bounded authorized CardTrader ingest.","auth":"Status and writes for a selected game require the configured CardTrader ingest service secret.","params":{"query":"`game` selects One Piece or Riftbound for GET status.","body":"`game` plus bounded discover/apply importer controls for POST."}},{"path":"/api/cardtrader-disconnect","methods":["POST"],"family":"cardtrader","purpose":"Disconnect the authenticated seller CardTrader integration.","auth":"Required Firebase bearer token.","params":{"body":"No required fields."}},{"path":"/api/cardtrader-webhook/:uid","methods":["POST"],"family":"cardtrader","purpose":"Receive CardTrader order webhooks for a connected seller and decrement linked Pokoin inventory.","auth":"CardTrader Signature HMAC using the seller shared_secret.","params":{"path":"`uid` Firebase seller uid registered as the webhook URL suffix.","body":"Raw CardTrader webhook JSON (order.create / order.update / order.destroy)."},"rawBody":true},{"path":"/api/cardtrader-import-dry-run","methods":["POST"],"family":"cardtrader","purpose":"Read the authenticated seller CardTrader export and return a redacted import summary without writing inventory.","auth":"Required Firebase bearer token.","params":{"body":"Optional dry-run controls; no writes are performed."}},{"path":"/api/client-country","methods":["GET"],"family":"other","purpose":"ISO country from the edge IP header, for local eBay and Vinted links.","auth":"Public.","params":{"query":"No query parameters. Response is { country } or empty when the edge country is unknown."}},{"path":"/api/tcgplayer-redirect","methods":["GET"],"family":"other","purpose":"Open the TCGplayer product linked to a public card id.","auth":"Public.","params":{"query":"`id` public card_id. `format=json` returns { url, productId }. `game` for non-Pokemon catalogs."}},{"path":"/api/cardtrader-redirect","methods":["GET"],"family":"cardtrader","purpose":"Redirect a public card id or leftover ct_id to the CardTrader leftover blueprint page.","auth":"Public.","params":{"query":"`id` public card_id (ct_id × 2) or leftover ct_id. Optional `blueprintId` leftover. `game` for One Piece / Riftbound. `format=json` returns `{ url, ct_id }`."}},{"path":"/api/cardtrader-status","methods":["GET"],"family":"cardtrader","purpose":"Return safe CardTrader integration status for the authenticated seller.","auth":"Required Firebase bearer token.","params":{"query":"No required query parameters."}},{"path":"/api/create-pkn-checkout-session","methods":["POST"],"family":"payments","purpose":"Create or reconcile a Stripe Checkout session for buying PKN account balance.","auth":"Required Firebase bearer token.","params":{"body":"`pknAmount`, `fiatCents`, and `lookupKey` for new checkout; `checkoutSessionId` for reconciliation."}},{"path":"/api/crypto-pkn-purchase/:action","methods":["GET","POST"],"family":"payments","purpose":"Quote, request, and check crypto-to-PKN purchase flows.","auth":"Required Firebase bearer token.","params":{"path":"`action` is `quote`, `request`, or `status`.","body":"`asset` and `amountIn` for quote; `quoteId` and `depositTxHash` for request.","query":"`requestId` optional for status."}},{"path":"/api/crypto-pkn-sale/:action","methods":["GET","POST"],"family":"payments","purpose":"Quote, request, and check PKN-to-crypto sale flows.","auth":"Required Firebase bearer token.","params":{"path":"`action` is `quote`, `request`, or `status`.","body":"`asset` and `amountIn` for quote; `quoteId`, `depositTxHash`, and `payoutAddress` for request.","query":"`requestId` optional for status."}},{"path":"/api/deck-card-version-lookup","methods":["GET","POST","OPTIONS"],"family":"other","purpose":"Return ranked marketplace card versions for structured decklist card fields.","auth":"Public.","params":{"query":"`name`, `setCode`, `collectorNumber`, optional Limitless expansion fields, `language`, and `limit` are supported.","body":"Same fields as query parameters for POST."}},{"path":"/api/extension-card-search","methods":["POST","OPTIONS"],"family":"search","purpose":"Search marketplace cards from browser-extension scraped card fields.","auth":"Public.","params":{"body":"`query` or structured fields such as `name`, `collectorNumber`, `expansion`, `rarity`, `variation`, `language`, and `limit`."}},{"path":"/api/earn-pkn","methods":["POST","OPTIONS"],"family":"payments","purpose":"Receive Earn PKN sharding inquiries and email the completed form to Pokoin contact.","auth":"Public.","params":{"body":"`email`, `numberOfCards`, and `valueOfCards` required; optional `cardList`, `language`, and `conditions`."}},{"path":"/api/flutter-debug-logs","methods":["GET","POST"],"family":"debug","purpose":"Record and read protected Flutter client debug logs.","auth":"Required debug token or authorized debug/admin Firebase bearer token.","params":{"query":"GET filters include `limit`, `sessionId`, `userId`, `path`, `category`, and `eventName`.","body":"POST requires `sessionId` and `eventName`, with optional route/url/user/payload fields."}},{"path":"/api/forum","methods":["GET"],"family":"other","purpose":"Read forum categories, topic lists, or a single topic with posts.","auth":"Public.","params":{"query":"`categoryId` optional for topic lists, `topicId` optional for a single topic."}},{"path":"/api/forum-create-post","methods":["POST"],"family":"other","purpose":"Create an authenticated forum reply.","auth":"Required Firebase bearer token.","params":{"body":"`topicId` and post content fields."}},{"path":"/api/forum-create-topic","methods":["POST"],"family":"other","purpose":"Create an authenticated forum topic.","auth":"Required Firebase bearer token.","params":{"body":"Topic title/content/category fields."}},{"path":"/api/forum-upload-media","methods":["POST"],"family":"other","purpose":"Optimize forum image media and store it in R2.","auth":"Required Firebase bearer token.","params":{"body":"`imageBase64` and either `topicId` or `postId`."}},{"path":"/api/limitless-expansion-blueprints","methods":["GET"],"family":"catalog","purpose":"Return Limitless expansion-to-Pokoin blueprint mapping rows.","auth":"Public.","params":{"query":"`expansionKey`, `setCode`, `name`, `includeBlueprints=1`, and `limit` are supported."}},{"path":"/api/marketplace-artist-cards","methods":["GET"],"family":"catalog","purpose":"Return artist profile data and marketplace cards grouped by illustrator/artist attribution.","auth":"Public.","params":{"query":"`artistSlug` or `artist`; `limit` optional."}},{"path":"/api/marketplace-artist-suggestions","methods":["GET"],"family":"search","purpose":"Return marketplace artist suggestion rows for artist pages and admin review.","auth":"Public.","params":{"query":"Search/filter query parameters including artist text and limit."}},{"path":"/api/marketplace-suggest","methods":["GET","OPTIONS"],"family":"search","purpose":"Meili-only typeahead for pokoin-web: grouped printings, no SQL listing hydrate.","auth":"Public.","params":{"query":"q|query, game, limit (default 20, max 24 groups; popup capped at 20 printings), search_language|lang|language, print_language|printLanguage, match=all optional"},"returns":"query, game, groups[] of {name, printings[{id,set,number,rarity,image,href,...}]}, shown, count; Pokemon also globalCount and printLanguage. Empty/degraded responses can include meta.reason."},{"path":"/api/marketplace-autocomplete","methods":["POST","OPTIONS"],"family":"search","purpose":"Return ranked marketplace autocomplete/search suggestions with optional debug metadata.","auth":"Public for normal search; debug and personalization use optional Firebase bearer token.","params":{"body":"`search_term`/`query`, `result_limit`, `pool_limit`, `search_language`, optional previous context and debug fields."}},{"path":"/api/marketplace-blueprint-price","methods":["GET","OPTIONS"],"family":"other","purpose":"Return the public PKN floor price for a marketplace blueprint/card ID.","auth":"Public.","params":{"query":"`blueprintId` required; `cardId` accepted as an alias."}},{"path":"/api/marketplace-card-cheapest-price","methods":["GET","OPTIONS"],"family":"card","purpose":"Return the homepage-backed cheapest marketplace price for a card, including CardTrader cache availability.","auth":"Public.","params":{"query":"`cardId`, `cardIds`, `canonicalPath`, or structured `name`/`setName`/`collectorNumber`; `language` and bounded `limit` optional."}},{"path":"/api/marketplace-card-page","methods":["GET","OPTIONS"],"family":"page-bff","purpose":"React BFF: one card-detail payload (card, versions, versionCount, offers, cheapest, artist, canonicalPath).","auth":"Public.","params":{"query":"cardId|id required (public id), game, lang|language|search_language, cardSlug|slug, includeSales, includeOffers, includeSameAs, liveOffers (all flags default off; 1|true|yes enables), offerLimit (default 40, max 80), salesLimit (default 40, max 120)"},"returns":"card, game, version, visualTheme, versionCount, versions[], rarities[], neighbors.{prev,next}, sameAs[], offers[], cheapest, sales[], artist, canonicalPath, seo, lookup"},{"path":"/api/marketplace-card-seo","methods":["GET"],"family":"card","purpose":"Return server-rendered HTML metadata for marketplace card social previews.","auth":"Public.","params":{"query":"`cardId`, `cardSlug`, `language`, or `cardPath` depending on the rewrite source."}},{"path":"/api/marketplace-card-sales","methods":["GET"],"family":"card","purpose":"Daily sold-median series from CardTrader inferred comps. Pi aggregates the requested language/condition slice; default payload is series + available filter keys, not observation rows.","auth":"Public.","params":{"query":"`cardId` required. Optional `condition` / `cond` (NM, SP, MP, PL, Poor) and `language` / `lang` (EN, IT, JP, …). Series includes `sampleCount` (observations in the current slice) and per-day `sampleCount`. `includeRows=1` returns a capped observation sample; omit it for series-only. `limit` only applies with includeRows."}},{"path":"/api/marketplace-sales-pulse","methods":["GET","OPTIONS"],"family":"other","purpose":"Return the latest completed daily marketplace sales leaders plus a bounded activity trend.","auth":"Public.","params":{"query":"`metric=sales` (default, observed samples) or diagnostic `metric=quantity`; `limit` (max 24) and `days` (max 30) are optional."}},{"path":"/api/marketplace-card-last-median","methods":["GET"],"family":"card","purpose":"Return the latest UTC-day median inferred sold PKN for one or more marketplace cards from CardTrader removed-sale comps.","auth":"Public.","params":{"query":"`cardId` or comma-separated `cardIds` (max 40). `blueprintId` is accepted as an alias of `cardId`. Optional `condition` / `cond` and `language` / `lang` slice the last-day median the same way as marketplace-card-sales."}},{"path":"/api/marketplace-card-shortlink","methods":["GET","HEAD"],"family":"card","purpose":"Redirect our-id short links to canonical marketplace card URLs (our id = CardTrader ct_id * 2).","auth":"Public.","params":{"query":"`cardId` is our marketplace id (ct_id * 2) or leftover ct_id; `path` is `/marketplace/220962` or `/220962/slug`. See docs/marketplace-public-ids.md."}},{"path":"/api/marketplace-card-url","methods":["GET","HEAD"],"family":"card","purpose":"Return stored canonical_path. cardId is our marketplace id (ct_id * 2); path numbers are the same our-id.","auth":"Public.","params":{"query":"`cardId` (our id, or leftover ct_id) or `path` (our-id URL); `language` optional. See docs/marketplace-public-ids.md."}},{"path":"/api/marketplace-card-versions","methods":["GET"],"family":"card","purpose":"Return card detail/version rows for marketplace card pages.","auth":"Public.","params":{"query":"`cardId`, `sameAsCardId`, `cardSlug`, `expansionName`, `query`, `limit`, `productType`, and `language` are supported."}},{"path":"/api/marketplace-version-set","methods":["GET","OPTIONS"],"family":"card","purpose":"Return the pokoin_version_sets key, member_count, and printings for one public card id.","auth":"Public.","params":{"query":"`cardId` (public marketplace id)."}},{"path":"/api/marketplace-cardmarket-guess-review","methods":["GET"],"family":"cardmarket","purpose":"Return protected Cardmarket guess review data for search/debug operators.","auth":"Required authorized debug/admin Firebase bearer token.","params":{"query":"Review filters and pagination query parameters."}},{"path":"/api/marketplace-cart","methods":["POST"],"family":"commerce","purpose":"Record marketplace cart add/remove analytics with optional verified user context.","auth":"Public; optional Firebase bearer token attaches user UID when valid.","params":{"body":"`cardId` or `blueprintId`, `action`, and optional anonymous/session ID."}},{"path":"/api/marketplace-cards","methods":["GET"],"family":"search","purpose":"Return searchable marketplace card and product rows.","auth":"Public.","params":{"query":"`query`, `limit`, `language`, `productType`, and `productSearchOnly` are supported."}},{"path":"/api/marketplace-debug-artists","methods":["GET","POST"],"family":"debug","purpose":"Inspect and update marketplace artist enrichment/debug classification data.","auth":"Required authorized debug/admin Firebase bearer token.","params":{"query":"GET filters for artist debug views.","body":"`action` plus action-specific artist/classification payload for POST."}},{"path":"/api/marketplace-debug-cardtrader-blueprints","methods":["GET","POST"],"family":"cardtrader","purpose":"Inspect and enqueue protected CardTrader blueprint debug/import work.","auth":"Required authorized debug/admin Firebase bearer token.","params":{"query":"GET filters/status fields.","body":"POST queue/review payload fields."}},{"path":"/api/marketplace-debug-events","methods":["GET"],"family":"debug","purpose":"Return marketplace event analytics debug data.","auth":"Required authorized debug/admin Firebase bearer token.","params":{"query":"Event/card/search filters and limits."}},{"path":"/api/marketplace-debug-refinement","methods":["GET","POST"],"family":"debug","purpose":"Inspect and update marketplace search refinement/debug data.","auth":"Required authorized debug/admin Firebase bearer token.","params":{"query":"GET refinement filters.","body":"POST action-specific refinement payload."}},{"path":"/api/marketplace-image-log","methods":["GET","POST","OPTIONS"],"family":"debug","purpose":"Ring-buffer exact marketplace image URLs served or failed during navigation.","auth":"Public; POST is IP rate-limited. GET returns the in-memory buffer.","params":{"query":"GET `limit` (max 250).","body":"POST `url` required-ish; optional `cardId`, `ctId`, `source`, `status`, `error`, `route`."}},{"path":"/api/marketplace-event","methods":["POST"],"family":"commerce","purpose":"Record public marketplace interaction/search events and refresh hot-card aggregates opportunistically.","auth":"Public; optional Firebase bearer token attaches user UID when valid.","params":{"body":"`cardId`, `eventType`, optional `source` and bounded metadata."}},{"path":"/api/marketplace-expansion-symbols","methods":["GET","POST"],"family":"catalog","purpose":"Read or update marketplace expansion symbol metadata.","auth":"Required authorized debug/admin Firebase bearer token for both GET and POST.","params":{"query":"GET filters for expansions.","body":"Expansion symbol update fields for POST."}},{"path":"/api/marketplace-expansion-page","methods":["GET","OPTIONS"],"family":"page-bff","purpose":"React BFF: expansion metadata plus paginated singles for a set browse page.","auth":"Public.","params":{"query":"game; expansionName or slug; productType default card; limit (default 200, max 400), offset. Omit both name and slug for the expansion index (limit default 500, max 2000)."},"returns":"expansion { name, slug, cardCount }, cards[], total, count, hasMore, limit, offset. cardCount/total is stored catalog_card_count (grid singles with art), not the page size and not TCGDex printedTotal."},{"path":"/api/marketplace-expansions","methods":["GET"],"family":"catalog","purpose":"Return marketplace expansion list or detail snapshots.","auth":"Public.","params":{"query":"Expansion slug/id/detail filters and optional limit."}},{"path":"/api/marketplace-home","methods":["GET"],"family":"other","purpose":"Return marketplace home snapshot and carousel sections.","auth":"Public.","params":{"query":"`recentCardIds` optional comma-separated public ids; returned as sections.recentlySeenIds / spotlightIds."}},{"path":"/api/marketplace-rails","methods":["GET","OPTIONS"],"family":"other","purpose":"Pi browse rails. Public card_id only. No Supabase.","auth":"Public.","params":{"query":"`id` rail id (`new_cards`, `set:destined-rivals`)."}},{"path":"/api/marketplace-card-tiles","methods":["GET","OPTIONS"],"family":"other","purpose":"Pi card tiles by public id. No Supabase.","auth":"Public.","params":{"query":"`ids` comma-separated public card ids."}},{"path":"/api/marketplace-home-page","methods":["GET","OPTIONS"],"family":"page-bff","purpose":"React BFF: fast home carousels (newest English sets + hot blueprints). No CardTrader hydrate.","auth":"Public.","params":{"query":"game; recentCardIds optional comma-separated public ids; limit default 36, max 48"},"returns":"game, cards[], sections.{recentlySeenIds,bestSellerIds,featuredIds,newArrivalIds,spotlightIds}; optional source"},{"path":"/api/marketplace-competitive","methods":["GET"],"family":"catalog","purpose":"Return Limitless-backed competitive deck metagame, deck detail, tournament, standings, and pairings data for the marketplace competitive page.","auth":"Public.","params":{"query":"`game`, `format`, `year`, `limit`, and `includeGames=1` for the dashboard; `deckId` for public Limitless deck detail; `tournamentId` for tournament standings/pairings detail."}},{"path":"/api/marketplace-hot-blueprints","methods":["GET"],"family":"catalog","purpose":"Return hot marketplace blueprint rows and rolling interaction counts.","auth":"Public.","params":{"query":"`includeCards` and `limit` optional."}},{"path":"/api/marketplace-listings","methods":["GET","POST","PATCH"],"family":"commerce","purpose":"Read public active listings and create/update/decrement authenticated seller listings.","auth":"Public for active listing reads; writes and seller-owned reads require Firebase bearer token. Reserve listings require reserve role.","params":{"query":"`cardId`, `sellerUid`, `sellerUsername`, `id`, `action`, and `limit` supported. `nativeOnly=1` (or `live=0`) skips live CardTrader merge on public card reads.","body":"Create/update listing fields such as `cardId`, seller display fields, condition, language, `pricePkn`, quantity, and source flags."}},{"path":"/api/marketplace-listings-csv","methods":["GET","POST"],"family":"commerce","purpose":"Export authenticated seller inventory or preview/import PowerTools, Cardmarket, and CardTrader stock CSV files.","auth":"Required Firebase bearer token; all reads and writes are scoped to the decoded seller uid.","params":{"query":"`format` selects `powertools`, `cardmarket`, or `cardtrader` for GET export.","body":"`csv`, optional `format`, `stackSize`, `priceMode`, and `dryRun` for POST import."}},{"path":"/api/marketplace-collection-summary","methods":["GET"],"family":"other","purpose":"Authenticated owned-card totals for dashboard Portfolio. Admin Firestore read; uid only from verified bearer.","auth":"Firebase bearer token required. Never accepts a client uid.","params":{"query":"None. Owner is decoded.uid from the Authorization bearer."}},{"path":"/api/marketplace-collection","methods":["GET"],"family":"other","purpose":"Authenticated owned holdings rows for /collection (physical + NFT). Admin Firestore read; uid only from verified bearer.","auth":"Firebase bearer token required. Never accepts a client uid.","params":{"query":"None. Owner is decoded.uid from the Authorization bearer."}},{"path":"/api/marketplace-portfolio","methods":["GET","OPTIONS"],"family":"page-bff","purpose":"React BFF: Pokoin catalog + native PKN overlay (Portfolio / Explore). No CardTrader leftover images. No USD.","auth":"Public.","params":{"query":"game; id optional; limit Pokemon default 400/max 500, satellite default 2000/max 2500"},"returns":"game, currency, totals, games[], items[], holdings[]. Catalog explore data, not authenticated personal holdings."},{"path":"/api/marketplace-orders","methods":["POST"],"family":"commerce","purpose":"Create paid marketplace orders, decrement listings, credit sellers, and send seller notifications.","auth":"Required Firebase bearer token.","params":{"query":"`action=checkout` default, `action=notify-sellers` for notification retry/admin flows.","body":"`items`, `subtotalPkn`, and `totalPkn` for checkout; notification payload for notify-sellers."}},{"path":"/api/marketplace-search-page","methods":["GET","OPTIONS"],"family":"page-bff","purpose":"React BFF: Meili/SQL search results with pagination and product facets.","auth":"Public.","params":{"query":"query|q, game, limit (default/max 100), offset (default 0), productType, productSearchOnly=1, search_language|lang|language, print_language|printLanguage, includeFacets (default enabled; 0 disables)"},"returns":"query, game, productType, productSearchOnly, lang, cards[], facets.products[], count, total (nullable), hasMore, limit, offset. Identity/image hydrate plus cached cheapest PKN; no live shop-listing read."},{"path":"/api/marketplace-search-candidates","methods":["POST"],"family":"search","purpose":"Return split/search candidate rows for marketplace search diagnostics and clients.","auth":"Public for normal search; debug output requires authorized debug/admin Firebase bearer token.","params":{"body":"`search_term`/`searchTerm`, `result_limit`, `result_offset`, `search_language`, optional previous context/debug fields."}},{"path":"/api/marketplace-recents","methods":["GET","PUT","POST","OPTIONS"],"family":"commerce","purpose":"Signed-in recently seen public card ids. Writer is nezopt 15T. Tile JSON is not stored.","auth":"Firebase bearer token required.","params":{"body":"`cardIds` array (PUT/POST), optional `cardId` prepended. Max 24 unique public ids."}},{"path":"/api/marketplace-watchlist","methods":["POST"],"family":"commerce","purpose":"Record marketplace watchlist add/remove analytics with optional verified user context.","auth":"Public; optional Firebase bearer token attaches user UID when valid.","params":{"body":"`cardId` or `blueprintId`, `action`, and optional client context."}},{"path":"/api/money-request","methods":["GET","POST"],"family":"other","purpose":"Create, list, pay, decline, cancel, and mark notifications for authenticated PKN requests.","auth":"Required Firebase bearer token; request parties and status transitions are enforced server-side.","params":{"query":"`action` selects list, notifications, create, pay, decline, cancel, or read-notifications.","body":"Create uses `recipientUsername`, `amountPkn`, optional `note` and `clientToken`; mutations use `requestId`."}},{"path":"/api/pokoin-assistant","methods":["POST"],"family":"assistant","purpose":"Answer Pokontact assistant chat requests with marketplace grounding and optional service handoff.","auth":"Public; optional Firebase bearer token attaches verified user context.","params":{"body":"`message` required, optional `messages`, `page`, `pageContext`, and `username`."}},{"path":"/api/user-current-page","methods":["GET","POST"],"family":"auth","purpose":"Store or read the current internal Pokoin page for an assistant browser session.","auth":"Public anonymous session ID; optional Firebase bearer token scopes reads/writes to the verified user.","params":{"query":"GET accepts `sessionId` or `session_id`.","body":"POST requires `sessionId` and a safe internal `path` or Pokoin URL; optional `source`."}},{"path":"/api/social-autopost","methods":["POST"],"family":"social","purpose":"Post supplied Pokoin social copy or card payloads to configured Telegram and X channels, optionally using the dedicated peer2 social copy agent.","auth":"Required shared social autopost secret, CRON_SECRET bearer, or authorized debug/admin Firebase bearer token.","params":{"body":"`targets`, `message`, optional card fields, `dryRun`, `sendPhoto`, `silent`, and `useAgent`."}},{"path":"/api/social-autopost/hot-card","methods":["GET","POST"],"family":"social","purpose":"Select a hot Pokoin marketplace card and post it to configured social channels, optionally using the dedicated peer2 social copy agent.","auth":"Required shared social autopost secret, CRON_SECRET bearer, or authorized debug/admin Firebase bearer token.","params":{"query":"`targets`, `window`, `limit`, `dryRun`, `sendPhoto`, `silent`, and `useAgent` for GET.","body":"Same fields as query for POST; optional `message`, `hook`, and `hashtags` override generated copy."}},{"path":"/api/social-post-agent","methods":["POST"],"family":"social","purpose":"Generate Telegram and X copy through the dedicated peer2 social agent without posting to providers.","auth":"Required shared social autopost secret, CRON_SECRET bearer, or authorized debug/admin Firebase bearer token.","params":{"body":"`targets`, card/message fields, and optional `prompt`; returns generated copy and deterministic fallback metadata."}},{"path":"/api/register-email","methods":["POST"],"family":"other","purpose":"Start email/password signup by storing pending signup data and sending verification mail, or resend a pending verification (resend: true) with per-email rate limiting.","auth":"Public.","params":{"body":"`email`, `password`, optional `username` and `redirectPath`; or `resend: true` with `email` only."}},{"path":"/api/remove-profile-picture","methods":["POST"],"family":"other","purpose":"Remove the authenticated user custom profile picture and delete old R2 object when present.","auth":"Required Firebase bearer token.","params":{"body":"No required fields."}},{"path":"/api/request-pkn-withdraw","methods":["POST"],"family":"other","purpose":"Withdraw PKN from site balance to a linked native PKN address.","auth":"Required Firebase bearer token.","params":{"body":"Withdrawal amount/address or linked-wallet withdrawal fields used by the wallet UI."}},{"path":"/api/scan-batch","methods":["GET","POST","OPTIONS"],"family":"scan","purpose":"Scan Connect staged batch: snapshot, Batch Defaults, row edits (duplicate, merge undo), idempotent submit to marketplace_user_listings.","auth":"Required Firebase bearer token; rows are scoped to the seller uid.","params":{"query":"`batchId`; `list=open`; `action=image&itemId=` returns the scan JPEG.","body":"`action` = defaults | item | add | remove | restore | duplicate | unmerge | submit | discard (pokoin-web docs/SCAN_LISTING_WORKFLOW.md)."}},{"path":"/api/scan-pair","methods":["POST","OPTIONS"],"family":"scan","purpose":"Phone claims a 4-digit Scan Connect pairing code or QR secret and receives a session-scoped phone token.","auth":"Public. Postgres-backed per-IP and global failure limits; identical error for wrong, expired and used codes.","params":{"body":"`pin` (4 digits) or `qr`, optional `device` label."}},{"path":"/api/scan-phone","methods":["POST","OPTIONS"],"family":"scan","purpose":"Paired phone heartbeat, idempotent scan events (scanEventId), and leave.","auth":"`Authorization: Scan <phoneToken>` from /api/scan-pair. No Firebase.","params":{"query":"`action` = heartbeat | scan | leave.","body":"scan: `scanEventId`, `clientSequence`, `capturedAt`, `clockOffsetMs`, `recognition.hits`, optional `image` (base64 JPEG ≤ 45 KB), `timings`."}},{"path":"/api/scan-session","methods":["GET","POST","OPTIONS"],"family":"scan","purpose":"Desktop Scan Session: start/resume with pairing code, regenerate code, disconnect phone, pause, end.","auth":"Required Firebase bearer token.","params":{"query":"`sessionId` (GET), `action` = start | pairing | disconnect | pause | end (POST).","body":"`batchId`, `sessionId`, `paused`, `reason`."}},{"path":"/api/scan-stream","methods":["GET","OPTIONS"],"family":"scan","purpose":"Server-sent change stream for one Scan Batch (rows, defaults, session) with cursor replay; closes after 55 s.","auth":"Required Firebase bearer token.","params":{"query":"`batchId`, `after` cursor."}},{"path":"/api/ensure-username","methods":["POST"],"family":"auth","purpose":"Return the caller username registered in usernames/{name} (repairing or assigning it), or claim a new exact username.","auth":"Required Firebase bearer token.","params":{"body":"Optional `username` (3-32 a-z0-9) to claim; empty body ensures the current one."}},{"path":"/api/search-recipient-emails","methods":["GET","POST"],"family":"other","purpose":"Search usernames for transfers, ensure a username, or update the authenticated user username.","auth":"Required Firebase bearer token.","params":{"query":"`q` for GET username search.","body":"Optional `username` for POST update; empty POST ensures a unique username."}},{"path":"/api/searchbar-cancel","methods":["GET","POST"],"family":"search","purpose":"Mark a searchbar session as cancelled for in-process search cancellation checks.","auth":"Public.","params":{"query":"`search_session_id`/`sessionId` supported.","body":"`search_session_id`/`sessionId` supported."}},{"path":"/api/searchbar-cards","methods":["GET","POST"],"family":"search","purpose":"Stable wrapper around marketplace autocomplete ranking for searchbar experiments and clients.","auth":"Public; debug/personalization may use optional Firebase bearer token.","params":{"query":"`query`, `search_language`, `limit`, and `pool_limit` supported for GET.","body":"`query`, `search_language`, `limit`, `pool_limit`, previous context, debug, and mode fields for POST."}},{"path":"/api/searchbar-token-predict","methods":["GET","POST","OPTIONS"],"family":"search","purpose":"Return lightweight card-name token predictions for active typed fragments.","auth":"Public.","params":{"query":"`query`, `search_language`, and `limit` for GET.","body":"`query`, `search_language`, `limit`, and optional previous prediction context for POST."}},{"path":"/api/signup-notification","methods":["POST"],"family":"other","purpose":"Send signup notification email for the authenticated user once.","auth":"Required Firebase bearer token.","params":{"body":"Optional signup/profile metadata."}},{"path":"/api/stripe-webhook","methods":["POST"],"family":"payments","purpose":"Handle Stripe Checkout webhooks and credit completed PKN purchases.","auth":"Stripe webhook signature using raw request body.","params":{"body":"Raw Stripe webhook payload. Do not pre-parse JSON before signature verification."},"rawBody":true},{"path":"/api/top-up-account-balance","methods":["POST"],"family":"payments","purpose":"Verify a native PKN funding transaction and credit authenticated site balance.","auth":"Required Firebase bearer token.","params":{"body":"`amountPkn`, `fundingTxHash`, and optional `reconcileRecent`."}},{"path":"/api/trainingai-card-classify","methods":["POST","OPTIONS"],"family":"assistant","purpose":"Proxy card image classification requests to the Pokoin TrainingAI Oracle classifier or Hugging Face Space fallback.","auth":"Public by default; protect upstream classifier with TRAININGAI_HF_TOKEN when private.","params":{"body":"`imageBase64` JSON or multipart image upload required; `topK`/`top_k` optional from 1 to 10."}},{"path":"/api/transfer-account-balance","methods":["POST"],"family":"other","purpose":"Transfer PKN site balance from the authenticated user to another Pokoin account.","auth":"Required Firebase bearer token.","params":{"body":"Recipient username/email/uid and PKN amount fields."}},{"path":"/api/unlock-silver","methods":["POST"],"family":"other","purpose":"Unlock Silver status/features for the authenticated account.","auth":"Required Firebase bearer token.","params":{"body":"Unlock request fields used by the client."}},{"path":"/api/upload-profile-picture","methods":["POST"],"family":"other","purpose":"Optimize an uploaded profile picture and store it in R2.","auth":"Required Firebase bearer token.","params":{"body":"`imageBase64` required, max 6 MB source image."}},{"path":"/api/verify-email-signup","methods":["POST"],"family":"other","purpose":"Idempotently finalize a verified email signup into an ACTIVE Pokoin account (Firebase user, pok_email_verified claim, username claim, balances) and return a sign-in custom token.","auth":"Public token verification.","params":{"body":"Signup verification `token`."}},{"path":"/api/wallet-auth/nonce","methods":["POST"],"family":"other","purpose":"Create a nonce challenge for wallet sign-in.","auth":"Public.","params":{"body":"Wallet address and client challenge metadata."}},{"path":"/api/wallet-auth/verify","methods":["POST"],"family":"other","purpose":"Verify a signed wallet nonce and sign in/create the corresponding Firebase user.","auth":"Public signed wallet challenge.","params":{"body":"Wallet address, signature, nonce/session fields, and optional profile fields."}},{"path":"/api/wallet-link","methods":["POST"],"family":"other","purpose":"Link a wallet to the authenticated Firebase account.","auth":"Required Firebase bearer token.","params":{"body":"Wallet address/signature/session fields."}},{"path":"/api/wallet-link/complete","methods":["POST"],"family":"other","purpose":"Complete a wallet-link session from a signed wallet payload.","auth":"Signed wallet-link session payload.","params":{"body":"Wallet-link session id, address, signature, and profile fields."}},{"path":"/api/wallet-link/session","methods":["POST"],"family":"other","purpose":"Create a wallet-link session for an authenticated Firebase account.","auth":"Required Firebase bearer token.","params":{"body":"Wallet address and session metadata."}},{"path":"/api/wpkn-exchange/:action","methods":["GET","POST"],"family":"payments","purpose":"Quote, request, and check native PKN/wPKN exchange flows.","auth":"Required Firebase bearer token.","params":{"path":"`action` is `quote`, `request`, or `status`.","body":"`direction`, `amountIn`, `quoteId`, and `toAddress` depending on action.","query":"`requestId` optional for status."}},{"path":"/api/wpkn-pkn-quote","methods":["GET","POST"],"family":"payments","purpose":"Return a public wPKN/PKN market quote from GeckoTerminal plus the configured PKN USD price.","auth":"Public.","params":{"query":"`direction`, `amountIn`.","body":"`direction`, `amountIn` (POST)."}},{"path":"/api/marketplace-seller-shop","methods":["GET"],"family":"other","purpose":"Public seller shop with server total/unique + offset pagination and CT-like filters.","auth":"Public. sellerUsername required.","params":{"query":"sellerUsername required; game, limit (max 100), offset, q, condition, language, sort.","body":"none"}},{"path":"/api/cardtrader-sync","methods":["GET","POST"],"family":"cardtrader","purpose":"Reconcile the seller's complete CardTrader inventory into Pokoin (import/link/update/remove linked stock only).","auth":"Required Firebase bearer token; seller must have CardTrader connected.","params":{"query":"GET returns last sync status; POST runs reconcile.","body":"none"}},{"path":"/api/cardtrader-assets","methods":["GET"],"family":"cardtrader","purpose":"Signed-in seller's CardTrader 1-Day Ready inventory as dashboard assets (never Pokoin listings).","auth":"Required Firebase bearer token; uid from verified token only.","params":{"query":"none","body":"none"}},{"path":"/api/marketplace-portfolio-history","methods":["GET"],"family":"other","purpose":"Stored daily collection value: wallet on the days it moved, plus the homepage minimum of 1-Day Ready cards summed once per UTC day.","auth":"Required Firebase bearer token; uid from verified token only.","params":{"query":"none","body":"none"}},{"path":"/api/poko-market","methods":["POST"],"family":"other","purpose":"Poko market intelligence: read-only card resolution, sold-market quotes, liquidity, artist collection quotes, snapshot. One shared tool surface for every Poko channel.","auth":"Required service bearer token: POKO_MARKET_SERVICE_TOKEN, or POKONTACT_SERVICE_TOKEN (same secret, already in the Pi container env). Server-to-server only; browsers never receive it.","params":{"query":"none","body":"{ tool: resolve_card|card_quote|card_ocr|card_liquidity|collection_quote|suggest_cards|market_snapshot|top_movers, params } with tool-specific fields (query, cardId, condition, language, artist, subject, excludeCardId, limit, days, direction)."}},{"path":"/api/poko-personal-context","methods":["POST"],"family":"other","purpose":"Authenticated personal marketplace snapshot for Poko intent: desk, recents, watchlist, cart, inventory, collection. Firebase users sync browser cart/watchlist; Hermes service token loads by firebaseUid for linked Telegram/Discord.","auth":"Firebase bearer (user) or POKO_MARKET_SERVICE_TOKEN / POKONTACT_SERVICE_TOKEN (service + firebaseUid).","params":{"query":"none","body":"{ action: get|sync, watchlistIds?, cart?, desk?, firebaseUid? }"}},{"path":"/api/poko-chat","methods":["GET","POST"],"family":"other","purpose":"Website Poko assistant chat (Messages + dock). Firebase-authed Hermes proxy; persists transcript to Firestore poko_conversations/{uid}/events for cross-device sync. No local FAQ or market-tool fallback.","auth":"Required Firebase bearer token. Browser never receives the Hermes service token.","params":{"query":"action=history&before? (GET)","body":"{ message, cards?: [{ cardId, name, setName, condition, language }], images?: string[], sessionId?, pageContext? } (POST)"}},{"path":"/api/marketplace-seller-settings","methods":["GET","POST"],"family":"other","purpose":"Seller ship-from country (ISO-2) and Stripe Connect readiness.","auth":"Required Firebase bearer token.","params":{"query":"none","body":"POST { shipFromCountry }"}},{"path":"/api/account-addresses","methods":["GET","POST","PUT","DELETE"],"family":"other","purpose":"Encrypted buyer shipping addresses; countryCode stays queryable outside ciphertext.","auth":"Required Firebase bearer token; owner only.","params":{"query":"id for PUT/DELETE; reveal=1 to decrypt for owner","body":"Address fields + optional isDefault/label"}},{"path":"/api/marketplace-checkout-quote","methods":["POST"],"family":"other","purpose":"Per-seller shipment quotes from seeded rate tables. Fail closed when route missing.","auth":"Required Firebase bearer token.","params":{"query":"none","body":"{ items, shippingAddressId }"}},{"path":"/api/stripe-connect-onboard","methods":["GET","POST"],"family":"payments","purpose":"Stripe Connect Express onboarding and READY status sync.","auth":"Required Firebase bearer token.","params":{"query":"none","body":"Optional returnUrl/refreshUrl"}},{"path":"/api/create-order-checkout-session","methods":["POST"],"family":"other","purpose":"Freeze EUR order + encrypted address snapshot, open Stripe Checkout Session.","auth":"Required Firebase bearer token.","params":{"query":"none","body":"{ items, shippingAddressId, buyerEmail? }"}},{"path":"/api/poko-connect","methods":["POST"],"family":"other","purpose":"Link a signed-in Pokoin account to a Telegram user for the Poko assistant: create link code (Firebase user), redeem/status/unlink (service token via Hermes).","auth":"create_code: Firebase bearer token. redeem/status/unlink: service bearer token (POKO_MARKET_SERVICE_TOKEN or POKONTACT_SERVICE_TOKEN).","params":{"query":"none","body":"{ action: create_code|redeem|status|unlink, ... } — create_code takes no params; redeem takes code + telegram { userId, username, displayName }; status/unlink take telegramUserId."}},{"path":"/api/marketplace-associate","methods":["GET"],"family":"other","purpose":"Associate desk: role (distributor, ambassador, …), campaign window, and live royalty earnings on sales made by Italian sellers to Italian buyers.","auth":"Required Firebase bearer token; caller email must exist in public.marketplace_associates, else 403.","params":{"query":"none","body":"none"}},{"path":"/api/marketplace-referral","methods":["GET","POST"],"family":"other","purpose":"Invite & Earn: the caller's invite code, invited collectors and 20 PKN rewards (settled on read), plus Ambassador mission progress and tier. POST action=claim attaches a new account to an inviter.","auth":"Required Firebase bearer token.","params":{"query":"none","body":"POST {action:'claim', code:'<inviter username>'}"}},{"path":"/api/marketplace-associate-suggest","methods":["GET"],"family":"search","purpose":"Users-search supplement: active associates roster matches by display name or email prefix (public name, username, role only).","auth":"Public. q (min 2 chars) optional.","params":{"query":"q prefix to match.","body":"none"}},{"path":"/api/marketplace-native-sales","methods":["GET"],"family":"other","purpose":"Public Sold on Pokoin rows for a card desk: paid native PKN/EUR orders, no buyer identity.","auth":"Public. cardId required.","params":{"query":"cardId required; limit (max 50).","body":"none"}},{"path":"/api/pokoin-partner","methods":["GET","POST"],"family":"other","purpose":"Pokoin Flex partner-store directory (public) and Partner app intake/bag/handoff contract (mutations coming_soon).","auth":"directory/contract public; intake/bag/receive-hub/handoff/pending will require partner auth when live.","params":{"query":"action=directory|contract|pending (storeId when partner auth exists).","body":"action plus storeId, parcelQr/bagQr/pickupQr as documented by action=contract."}},{"path":"/api/scan/identify","methods":["POST","OPTIONS"],"family":"other","purpose":"Card recognition (YOLO detect + Milo embed) for the website, Scan Connect and the app. nezopt GPU first, Pi CPU fallback.","auth":"Public; per-IP limit in the recognition worker.","params":{"query":"catalog, top_k (1-8), live, multi, album","body":"multipart/form-data field `file` (≤4 MB image)"},"rawBody":true},{"path":"/api/scan/identify-album","methods":["POST","OPTIONS"],"family":"other","purpose":"Recognize several photos of one album page at once.","auth":"Public; per-IP limit in the recognition worker.","params":{"query":"top_k, live","body":"multipart/form-data field `file` repeated"},"rawBody":true},{"path":"/api/scan/catalogs","methods":["GET"],"family":"other","purpose":"Recognition catalogs (game × language) the scanner can switch between.","auth":"Public; per-IP limit in the recognition worker.","params":{"query":"none"}},{"path":"/api/scan/health","methods":["GET"],"family":"other","purpose":"Which recognition workers answer (nezopt GPU, Pi CPU).","auth":"Public; per-IP limit in the recognition worker.","params":{"query":"none"}},{"path":"/api/marketplace-price-check","methods":["GET"],"family":"other","purpose":"PowerTools-style pricer for the MyPokoin listings board: per-listing market comps.","auth":"Bearer required. Caller's own listings are excluded from the Pokoin cheapest.","params":{"query":"items required: comma list of cardId[:COND:LANG], max 100; excludeSellerUid optional (defaults to the bearer uid).","body":"none"}},{"path":"/api/marketplace-pricing-strategies","methods":["GET","POST","DELETE"],"family":"other","purpose":"PowerTools-style pricing strategies + pricer defaults stored on users/{uid}.","auth":"Bearer required; strategies belong to the bearer uid.","params":{"query":"DELETE requires id.","body":"POST: {strategy} upserts one strategy (empty id mints one); {pricerSettings} saves the pricer defaults; both allowed."}},{"path":"/api/cardtrader-zero","methods":["GET"],"family":"cardtrader","purpose":"CardTrader Zero picking list: the weekly merged paid Zero order (the Thursday shipment) plus hub_pending Zero sales still waiting for the next merge, with MyPokoin locations and an optional Power Tools picking overlay.","auth":"Required Firebase bearer token; seller must have CardTrader connected (404 cardtrader_not_connected otherwise).","params":{"query":"none","body":"none"}},{"path":"/api/cardtrader-zero-pick","methods":["POST"],"family":"cardtrader","purpose":"CardTrader Zero pick of one copy: records which copy left the box in Postgres cardtrader_zero_picks and moves inventory (finalize clears the emptied copy's location; swap moves quantity from the picked identical copy to the copy the sale took, both outbox events in one transaction; undo reverses the row; replays are idempotent). PR A bodies (picked counts, issues, reset) and lines with no Pokoin listing stay on Firestore cardtrader_zero_picks/{uid}_{orderId} and never touch stock.","auth":"Required Firebase bearer token; picks belong to the bearer uid.","params":{"query":"none","body":"{ orderId, itemId, lineKey, listingId, allocatedListingId?, quantity 1..999, action: pick|undo, orderCode?, unitCents?, currency? } -> { ok, pick, listings, inventory: removed|swapped|restored|unchanged }; 409 not_identical / insufficient_quantity / allocated_copy_gone. Or PR A's { orderId, lineKey, picked?, issue? } / { orderId, reset: true }."}},{"path":"/api/cardtrader-zero-sort","methods":["POST"],"family":"cardtrader","purpose":"Save the seller's CardTrader Zero picking sort (Power Tools sort model: up to four levels such as game.asc, location.desc, cn.asc, product.rarity.asc) and smartLocations on Firestore cardtrader_zero_settings/{uid}. No CardTrader or Power Tools call.","auth":"Required Firebase bearer token; settings belong to the bearer uid.","params":{"query":"none","body":"{ fields: [<= 4 sort values], smartLocations?: bool } or { reset: true }; 400 invalid_sorting_fields / invalid_sorting_field / duplicate_sorting_field. -> { ok, sorting: { fields, defaultFields, smartLocations } }"}},{"path":"/api/cardtrader-zero-pt-sort","methods":["POST"],"family":"cardtrader","purpose":"User-triggered Apply sort: writes the Pokoin picking order to the seller's Power Tools picking list with PUT user/order/updateSorting (picked articles by updatedAt, then to-pick in the given sequence; the list's sortingFields unchanged), reads it back, and returns before/after so the UI can restore. dryRun reads only. Throttled to one write per 15 s; never retried.","auth":"Required Firebase bearer token; uses the bearer's stored Power Tools session.","params":{"query":"none","body":"{ orderId, listId, sequence: [itemId], dryRun?, expectCount } -> { ok, before, after, requested, applied } or { ok, dryRun, before, items }; 409 pt_sort_mismatch / pt_sort_count_changed / powertools_session_expired / powertools_cardtrader_not_connected / pt_picking_list_missing; 429 pt_sort_throttled; 502 other."}},{"path":"/api/cardtrader-zero-picked","methods":["GET"],"family":"cardtrader","purpose":"The seller's picked CardTrader Zero copies (cardtrader_zero_picks state picked), newest first, 100 a page, joined with the listing for name, set, number, facets and image. Picked rows are not listings: never stock, never pushed.","auth":"Required Firebase bearer token; rows of the bearer uid.","params":{"query":"offset? -> { rows, nextOffset }","body":"none"}},{"path":"/api/powertools-connect","methods":["GET","POST","DELETE"],"family":"other","purpose":"Power Tools session for the seller: GET status, POST signs in (email+password via Outseta, or a pasted jwt session) and stores only the encrypted session, DELETE forgets it.","auth":"Required Firebase bearer token; seller uid from token only. Failed password sign-ins are throttled per uid.","params":{"query":"none","body":"POST: email + password, or session (jwt cookie value)."}},{"path":"/api/poko-bets","methods":["POST"],"family":"other","purpose":"Poko Discord LoL bets in real site PKN: consent, balance check + stake into a per-round escrow, server-computed pari-mutuel settle/refund with ledger entries.","auth":"Service bearer token only (POKO_MARKET_SERVICE_TOKEN or POKONTACT_SERVICE_TOKEN). Only Discord users linked via poko-connect and opted in can stake or reveal a balance.","params":{"query":"none","body":"{ action: balances|set_consent|stake|close|settle|refund, ... } — stake takes guildId, gameId, discordUserId, side win|loss, whole amountPkn; settle takes outcome win|loss."}},{"path":"/api/marketplace-tcgplayer-history","methods":["GET"],"family":"other","purpose":"Daily TCGplayer aggregate price history with all explicitly mapped products and printing subtypes; USD, no condition-specific inference.","auth":"Bearer required.","params":{"query":"cardId required; game context; from/to YYYY-MM-DD optional.","body":"none"}},{"path":"/api/marketplace-card-price-history","methods":["GET","OPTIONS"],"family":"other","purpose":"Daily CardTrader lowest listed asks and separately sourced TCGplayer market quotes for one exact public printing. Never synthetic sales.","auth":"Public read-only.","params":{"query":"cardId required; game context; from/to YYYY-MM-DD optional, maximum 10 years.","body":"none"}},{"path":"/api/marketplace-live","methods":["GET"],"family":"other","purpose":"SSE listing quantity for an open card desk or seller inventory. Stays on the Pi process that publishes; the edge must not cache or overflow it.","auth":"Public filter by cardId or sellerUid. Payloads are the same fields already on the public listings table.","params":{"query":"cardId or sellerUid required.","body":"none"}},{"path":"/api/marketplace-cart-sync","methods":["GET","PUT","OPTIONS"],"family":"commerce","purpose":"Signed-in buyer's cart for pokoin.com/cart: cart lines, Saved for later and gift flag, synced across devices with revision-checked saves (409 CART_REV + current cart on a lost race).","auth":"Firebase bearer token required. Private, never cached.","params":{"query":"none","body":"PUT: items[] (max 400), saved[] (max 200), gift, baseRev."}},{"path":"/api/marketplace-recommendations","methods":["GET","OPTIONS"],"family":"other","purpose":"Personal card rails for every buyer (buy again, same-seller parcel, also carried, inspired by browsing, artists, trending, watchlist, recently viewed). Only buyable cards, each with the native offer to add.","auth":"Public; an optional Firebase bearer adds the account's cart, Recently Seen, watchlist snapshot and paid orders. Private, never cached.","params":{"query":"cart, recent, watch (card ids), sellers (uids), listings (listing ids), limit (4-30). Pokemon storefront only for now.","body":"none"}},{"path":"/api/user-photos/:kind/:uid/:file","methods":["GET"],"family":"other","purpose":"Proxy pokoin-user-photos from private R2. Chat requires auth (redirects to /auth); listing is public.","auth":"chat: Firebase bearer required. listing: public.","params":{"query":"kind, uid, file from path; optional key=user-photos/...","body":"none"}},{"path":"/api/marketplace-shipping-options","methods":["GET"],"family":"other","purpose":"Live Packlink carriers plus seeded letter rates for a seller→buyer lane. Cart/shipping preview.","auth":"Public read.","params":{"query":"fromCountry, toCountry, cards; optional fromZip, toZip","body":"none"}},{"path":"/api/news-comments","methods":["GET","POST"],"family":"other","purpose":"Pokoin News reader comments: list visible comments for an article; signed-in users post comments that wait for moderation.","auth":"GET public (signed-in callers also get their own pending comments). POST requires a Firebase bearer token; 5 comments per 10 minutes per user.","params":{"query":"articleId (art_…) for GET.","body":"POST { articleId, articlePath, body (2–1500 chars) }."}},{"path":"/api/scan/print","methods":["GET","OPTIONS"],"family":"other","purpose":"Print strip second pass: collect the collector number + language OCR for print ids returned by /api/scan/identify.","auth":"Public; per-IP limit in the recognition worker.","params":{"query":"ids (comma-separated print ids), wait_ms (0-3000, default 400)"}},{"path":"/api/cardtrader-deal-scan","methods":["GET"],"family":"cardtrader","purpose":"Scan a CardTrader seller (username or profile URL) for crazy asks vs facet-perfect sold comps (blueprint + condition + language + reverse/1st/graded).","auth":"Required Bearer DEAL_SCAN_TOKEN. Intended for the pokoin-ct-deals Vercel proxy; not a public browser endpoint.","params":{"query":"seller={ctUsername} or url={cardtrader profile URL}.","body":"none"}},{"path":"/api/news-event","methods":["POST"],"family":"other","purpose":"Pokoin News first-party reading beacons (list impressions/clicks, article views, scroll depth, active seconds) into Postgres news_events.","auth":"Public, no cookies. Visitor is a daily salted ip+UA hash; bots dropped; 120 beacons per 10 minutes per visitor (best effort). Answers 204 before writing.","params":{"query":"none","body":"POST { events: [{ type: impression|click|view|read|leave, articleId (art_…), articlePath, pv, source?, position?, depth?, seconds? }] } (max 40)."}},{"path":"/api/news-stats","methods":["GET"],"family":"other","purpose":"Admin-only Pokoin News reading stats for /news/dashboard: CTR, views, readers, scroll depth, time on page per article, per day and per card position.","auth":"Firebase bearer token of an admin (users/{uid} admin/isAdmin/role/roles, or admin claim). 401 signed out, 403 non-admin. Cache-Control private, no-store.","params":{"query":"days (1–365, default 30).","body":"none"}}]}