TypeScript SDK

@ripdotfun/cardos-sdk is the official client for every CardOS API — mystery packs, instant packs, custodial wallets, sell-back, redemption, revenue share, webhooks and the Card Data catalog. Zero runtime dependencies, ESM + CJS, and every response typed from the API's own field names. Node ≥ 20, Bun, Deno, Cloudflare Workers, Vercel Edge and the browser.

Every endpoint page in these docs carries an SDK tab next to its curl snippet, showing the same call through the client and tracking the same "Try it" inputs.

Install

pnpm add @ripdotfun/cardos-sdk
# or: npm i @ripdotfun/cardos-sdk · bun add @ripdotfun/cardos-sdk

Current version 0.1.2npm · GitHub.

Configure

import { CardOS } from "@ripdotfun/cardos-sdk";

const cardos = new CardOS({
  apiKey: process.env.CARDOS_API_KEY!,   // required — sent as X-API-Key
  environment: "production",             // "production" (default) | "staging"
  // baseUrl: "https://api.example/cardos", // your proxy; overrides environment
  game: "pokemon",                       // default game for cards/expansions/sealed
  timeoutMs: 30_000,                     // per request
  maxRetries: 2                          // 429 / 5xx / network
});

Keep the key on the server. A partner key can spend end-user balances, create buyback offers and register webhooks, so it belongs in a backend, a serverless function or an edge route — never in a browser or mobile bundle. The SDK runs in the browser only so that a same-origin proxy of yours can front the API: point baseUrl at that proxy and keep the key behind it. See Authentication.

OptionDefaultNotes
apiKeyYour key, rip_v1_…, sent as X-API-Key. Required.
environment"production""production"https://api.getcardos.com (Base mainnet), "staging"https://staging-service.rip.fun (Base Sepolia).
baseUrlYour own proxy or a mock server. Wins over environment.
game"pokemon"Default for Card Data calls; override per call with params.game.
timeoutMs30000Per request. Override per call.
maxRetries2GETs, DELETEs, and POSTs carrying an idempotency key.
fetch / defaultHeadersglobalThis.fetchA polyfill, a tracing wrapper or a test double; headers merged into every request.

Every method also takes per-call overrides — signal, timeoutMs, headers, maxRetries — alongside its own params.

Sell a pack

The headline flow: a tier is a price point, buying one draws real cards out of the vault, and the reveal is random. This is the custodial model — the user's balance pays, nobody signs anything. Endpoint pages: list tiers, custodial purchase, purchase status.

// 1. What you can sell. Price, EV and slot count are read live from the pool.
const tiers = await cardos.gacha.catalog({ active: true });

// 2. Buy one pack from the end user's CardOS balance. 202 — RESERVED.
const purchase = await cardos.gacha.purchase({
  tier_id: 3,
  external_user_id: user.id
});

// 3. Wait for the reveal. Polls with backoff; usually a few seconds.
const { items } = await cardos.gacha.waitForReveal(purchase.id);
// → [{ name: 'Umbreon VMAX', value_usd: '412.50', image_url: '…', card_id: 'swsh7-215' }]

// 4. What the user owns.
const collection = await cardos.gacha.collection({ external_user_id: user.id });

purchase() needs an Idempotency-Key; the SDK mints one per call so a network retry can never double-charge. Pass idempotency_key to own it — mint it when the user taps buy and reuse it across your retry loop. waitForReveal resolves on FULFILLED / PARTIALLY_FULFILLED and throws TerminalStateError on REFUNDED or FAILED. Poll for the "opening…" animation; drive fulfilment server-side off the purchase.fulfilled webhook.

Top up the balance with cardos.wallet.depositAddress(), watch it with wallet.balance(), and audit it with wallet.ledger().

Self-custody

The default model: the end user holds their own funds and their own cards. CardOS hands you the transactions, your wallet layer gets the user to send them, and you record the hash. The SDK never touches a private key. Endpoint pages: prepare, submit — and the full sequence on End-to-end flows.

// 1. Ask for the calls. Creates nothing.
const prep = await cardos.gacha.prepare({ wallet_address: user.wallet, tier_id: 3 });

// 2. The END USER sends these as transactions — signing a message does nothing
//    on-chain. Dispatch on call.kind ("erc20-approve", then "purchase"), never
//    on the description or the 4-byte selector.
const transaction_hash = await wallet.sendCalls(prep.calls);

// 3. Record the hash once it is mined. 202.
const purchase = await cardos.gacha.submit({
  wallet_address: user.wallet,
  tier_id: 3,
  transaction_hash
});
const revealed = await cardos.gacha.waitForReveal(purchase.id);

cardos.instant has the same prepare / submit pair for instant packs — a real booster pack, bought and opened in one transaction:

const packs = await cardos.instant.catalog();
const prep = await cardos.instant.prepare({
  wallet_address: user.wallet,
  packet_type_id: 41
});
const transaction_hash = await wallet.sendCalls(prep.calls);
const purchase = await cardos.instant.submit({
  wallet_address: user.wallet,
  packet_type_id: 41,
  transaction_hash
});

const delivered = await cardos.instant.waitForDelivery(purchase.id);
delivered.cards;

Instant calls carry no kind discriminator, so dispatch on position: calls[0] is the USDC approve, calls[1] the buy-and-open. Delivery is asynchronous — up to ~90 s while the VRF settles, which is why waitForDelivery allows 180 s.

Card Data

Search, get and price cards, expansions and sealed product across Pokémon, One Piece and Azuki. The q grammar and every filter are documented under Search & filtering; pricing under Pricing data. Endpoint pages: search cards, get a card, card pricing.

const cardos = new CardOS({ apiKey, game: "pokemon" });  // default game

const page = await cardos.cards.search({
  q: 'rarity:"Special Illustration Rare" raw_price:[100 TO *] -types:water',
  orderBy: "-raw_price",
  include: "prices",     // embeds pricing on every row — no surcharge
  page_size: 100
});
page.items;       // Card[]
page.totalCount;  // total matches

const card = await cardos.cards.get("swsh7-215", { include: "prices" });
const prices = await cardos.cards.prices("swsh7-215");
const printings = await cardos.cards.printings("swsh7-215");

const sets = await cardos.expansions.search({ q: "release_date:[2024-01-01 TO *]" });
const inSet = await cardos.expansions.cards("sv3pt5", { include: "prices" });
const boxes = await cardos.sealed.search({ q: "product_type:booster_box expansion.id:sv3pt5" });

One credit per catalog read, flat — a page of 100 with include: "prices" costs the same as one card without them. See API credits.

Pagination

// Card Data lists are a NumberedPage; the partner API returns an OffsetPage.
// Both are AsyncIterable over ITEMS, not pages.
for await (const card of await cardos.cards.search({ q: "expansion.id:sv3pt5" })) {
  console.log(card.id, card.name, card.pricing?.market);
}

const page = await cardos.gacha.listPurchases({ status: "FULFILLED", limit: 100 });
page.items;
page.hasMore;
const next = await page.next();   // the next page, or null
const all = await page.all();     // drain into one array

all() and for await keep fetching until the server says stop, so bound them: the partner API caps offset at 10 000 and Card Data caps page × page_size at 10 000. Narrow the query rather than paging deeper — see Best practices.

Webhooks

Register an endpoint with cardos.webhooks.register() — the signing_secret comes back exactly once, at creation — then verify every delivery. Verification lives in a separate entry point, @ripdotfun/cardos-sdk/webhooks, so a request handler can import it without the HTTP client. It is built on Web Crypto, so it runs unchanged on Node, Bun, Deno, Workers and Edge. The full event catalogue is in the webhooks guide.

import { constructEvent, WebhookSignatureError } from "@ripdotfun/cardos-sdk/webhooks";

// Express: express.raw, NEVER express.json() — re-serialising breaks the HMAC.
app.post("/hooks/cardos", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = await constructEvent({
      payload: req.body,                  // the raw Buffer (or a string)
      headers: req.headers,
      secret: process.env.CARDOS_WEBHOOK_SECRET!
    });
  } catch (err) {
    // Bad signature, tampered body, or older than 5 minutes → never retry it.
    return res.status(err instanceof WebhookSignatureError ? 400 : 500).end();
  }

  res.json({ received: true });           // ack fast, then do the slow work

  switch (event.event) {                  // discriminated union — data narrows
    case "purchase.fulfilled":
      for (const item of event.data.items ?? []) console.log(item.name, item.value_usd);
      break;
    case "purchase.refunded":
      refundInApp(event.data.purchase_id);
      break;
  }
  event.delivery_id;  // X-Mystery-Delivery — dedupe on this
});

verifyWebhookSignature() is the boolean-only version; it resolves true or throws WebhookSignatureError saying which check failed — it never resolves false, so a forgotten await cannot pass silently. Deliveries retry up to 6 times, so a slow consumer will see the same event twice: dedupe on event.delivery_id.

Errors

Every failed HTTP call throws a CardOSError, or a subclass keyed by status. Branch on err.code — the stable machine string — not on err.message, which is for humans and may change. Full envelope on Errors.

ClassStatusTypical code
ValidationError400invalid_tier, unknown_field, parse_error, invalid_address
AuthenticationError401unauthorized — missing / invalid key
InsufficientFundsError402insufficient_funds, insufficient_credits
PermissionError403missing scope, or a non-partner key
NotFoundError404not_found, token_not_found, value_unknown
ConflictError409sold_out, idempotency_mismatch, redemption_exists, webhook_limit
UnprocessableError422shape accepted, semantics rejected
RateLimitError429rate_limited — see retryAfterMs
ServiceUnavailableError503overloaded, timeout, relayer_disabled, instant_disabled
ServerError5xxinternal_error

Non-HTTP failures have their own classes, so a catch can tell "the API said no" from "we never heard back": ConnectionError, TimeoutError, PollTimeoutError, TerminalStateError, WebhookSignatureError. Every CardOSError carries status, code, message, details, requestId, retryAfterMs, retryable and the method / path that failed.

import {
  CardOSError, ConflictError, InsufficientFundsError, RateLimitError
} from "@ripdotfun/cardos-sdk";

try {
  await cardos.gacha.purchase({ tier_id: 3, external_user_id: user.id });
} catch (err) {
  if (err instanceof InsufficientFundsError) return topUp(user);
  if (err instanceof ConflictError && err.code === "sold_out") return offerAnotherTier();
  if (err instanceof RateLimitError) return retryAfter(err.retryAfterMs ?? 1000);
  if (err instanceof CardOSError) {
    console.error(err.status, err.code, err.message, err.requestId, err.details);
  }
  throw err;
}

The client retries automatically on 429, 502+ and network errors, honouring Retry-After with jittered exponential backoff — for GETs, DELETEs and POSTs that carry an idempotency key (which money-moving POSTs do by default). A POST without one is never retried. See Rate limits.

Polling

Every waitFor* helper takes the same options:

await cardos.gacha.waitForReveal(purchase.id, {
  intervalMs: 1_500,      // first gap between polls
  maxIntervalMs: 5_000,   // cap as it backs off
  backoffFactor: 1.5,
  timeoutMs: 120_000,     // give up after this long
  signal: ac.signal,
  onPoll: (value, attempt) => console.log(attempt, (value as Purchase).status)
});

Defaults are 1 500 ms → 5 000 ms with a 120 s timeout, except instant.waitForDelivery, which allows 180 s. Polling is for a foreground "opening…" experience; for anything server-side, use webhooks.

Types

Field names are the API's own — snake_case, no camelCase mirrors. Money is a string, never a JSON number ("12.500000"); never Number() it. Statuses are string-literal unions, so a switch is exhaustive and a typo is a compile error. No any appears in any exported signature, and types are exported alongside the client: import type { Purchase, Card, WebhookEvent } from "@ripdotfun/cardos-sdk".

Source, changelog and runnable examples: https://github.com/ripdotfun/cardos-sdk. Found a gap? Tell us.