@provable-games/collectables
v0.5.2
Published
SDK for Provable Games collectable NFTs on Starknet — typed API client, metadata generators, and bundled beast artwork
Downloads
984
Readme
@provable-games/collectables
TypeScript SDK for Provable Games collectable NFTs on Starknet — a typed client for the collectables API, on-chain-faithful metadata generators, and all beast artwork bundled in the package.
| Collection | Contract (Starknet mainnet) |
|---|---|
| Beasts | 0x046da8955829adf2bda310099a0063451923f02e648cf25a1203aac6335cf0e4 |
| Golden Token | 0x027838dea749f41c6f8a44fcfa791788e6101080c1b3cd646a361f653ad10e2d |
| Karat | 0x030f694df2a04e04cf3e1d3e79dd5aadfdaa77295bb007696222fc60a5e8730d |
npm install @provable-games/collectablesQuery collectables
import { CollectablesClient } from "@provable-games/collectables";
const client = new CollectablesClient(); // hosted API by default
// any filter/sort combination on metadata, keyset-paginated
const page = await client.beasts.list({
shiny: true,
type: "Magic",
levelGte: 20,
sort: "rank",
order: "asc",
limit: 50,
includeOwner: true,
});
const beast = await client.beasts.get(85559);
const golden = await client.goldenTokens.list({ airdropRound: 1 });Owner collections (single request)
Owner-scoped queries return the full holding in one response — no cursor loop:
const mine = await client.owner("0xYOUR_ADDRESS"); // { beasts, goldenTokens } — everything
const held = await client.beasts.byOwner("0xADDR"); // full page, nextCursor === null
// counts for an instant progress header, before/while the full list loads
const stats = await client.beasts.ownerStats("0xADDR");
// { total, uniqueBeastIds, shiny, animated, genesis }Grouped gallery
Pre-aggregate beasts into cards — one per species / type / tier / affix — each with its
count, shiny/animated tallies, and a representative token. Any beasts.list filter (incl.
search and the *In filters) applies before grouping. groups is global (the whole
collection); ownerGroups is scoped to one address. Both return Page<GroupAggregate> in a
single response (nextCursor is always null).
// global: how many of each affix pair exist, rarest first
const gallery = await client.beasts.groups({
groupBy: "full_name", // "beast_id" | "type" | "tier" | "prefix" | "suffix" | "full_name"
sort: "count", // "count" | "tier" | "power" | "rank" | "name" (default "count")
order: "desc",
tierIn: [1, 2], // filters apply before grouping
});
// same shape, scoped to one owner's holdings
const mine = await client.beasts.ownerGroups("0xADDR", { groupBy: "type" });
// each item:
// { key, label, beastId?, prefixName?, suffixName?,
// count, shinyCount, animatedCount, representative /* a Beast */ }Unnamed beasts collapse into a "__none__" group (label "No Special Name"), always sorted
last. The representative is the group's max-power token (tie-break: more shiny+animated, then
lowest token id).
Cursor pagination (general lists)
General token lists cap at 100 per page; walk them with the returned cursor:
let cursor: string | undefined;
do {
const p = await client.beasts.list({ sort: "power", order: "desc", cursor });
// ...use p.items
cursor = p.nextCursor ?? undefined;
} while (cursor);Custom fetch
Pass your own fetch (e.g. for auth headers or a proxy); it is invoked as-is:
new CollectablesClient({ fetch: myFetch });Images
All 75 beast species ship in the package in four variants
(static|animated × regular|shiny, PNG/GIF), the Golden Token SVG, and all
512 Karat SVGs (one unique on-chain image per token).
Recommended: copy them into your static assets once (add to your build or postinstall):
npx collectables copy-images ./public/collectablesimport { beastImageUrl, goldenTokenImageUrl, karatImageUrl } from "@provable-games/collectables";
// the beast's shiny/animated flags select the right variant automatically
beastImageUrl(beast, { baseUrl: "/collectables" });
// → "/collectables/beasts/animated/shiny/lich.gif"
karatImageUrl({ tokenId: 1, baseUrl: "/collectables" });
// → "/collectables/karat/1.svg"Other modes:
// bundler asset imports (advanced)
import { beastImageSpecifier, IMAGE_MANIFEST } from "@provable-games/collectables";
// Node.js servers (OG images, scripts) — separate subpath, browser-safe main entry
import { beastImageFilePath } from "@provable-games/collectables/node";Card image
The 250×350 beast "card" — the canonical NFT image the on-chain token_uri emits
(beasts/src/beast_svg.cairo) — is available as a self-contained SVG, so apps don't
re-implement it. The renderer is a faithful transcription of that on-chain generator:
same gold frame, shiny rainbow rim, tier-colored logo, crown/trophy, VT323 typeface and
stat tiles. It's a pure, browser-safe function — the client renders the card itself from
the lightweight beast data it already has; no server round-trip, no image bytes over the
wire beyond the (shared, cached) pixel art.
Drop into <img>
fetchBeastCardImgSrc returns a self-contained data: URI you put straight in an <img src>.
It fetches your hosted sprite once (browser-cached) and embeds it, so the card renders in the
sandboxed image context with no external requests — no inline SVG, no dangerouslySetInnerHTML,
nothing added to your bundle. The fetch in the name flags that it does I/O (and so is async):
import { fetchBeastCardImgSrc } from "@provable-games/collectables";
const src = await fetchBeastCardImgSrc(beast); // uses beast.image; or pass { art: "…url or data URI" }
// <img src={src} width={250} height={350} />
// React:
const [src, setSrc] = useState<string>();
useEffect(() => {
let alive = true;
fetchBeastCardImgSrc(beast).then((s) => alive && setSrc(s));
return () => { alive = false; };
}, [beast.image]);
return src ? <img src={src} style={{ width: "100%", height: "auto" }} /> : null;The beast needs an image field with your hosted sprite URL (or pass { art }). An already-data:
art skips the fetch entirely.
Why
<img>needs the embed: a card whose sprite is a URL renders with a blank art box inside<img>, because the browser's sandboxed image context blocks the sprite fetch.fetchBeastCardImgSrcsidesteps that by embedding the sprite up front. If you'd rather not fetch at all, render the SVG inline instead (below).
Render inline (lightest for a gallery)
Skip the embed entirely by inlining the SVG into the DOM — external art URLs load fine when the SVG is real markup rather than an image. Pull a page of beasts as JSON, render each card locally (~microseconds), and let the browser cache the per-species art; a 100-card page costs a few KB of JSON plus the shared art:
import { renderBeastCardSvg, beastImageUrl } from "@provable-games/collectables";
const svg = renderBeastCardSvg(beast, { art: beastImageUrl(beast, { baseUrl: "/collectables" }) });
return <div dangerouslySetInnerHTML={{ __html: svg }} />; // .card > svg { width:100%; height:auto }Bytes (NFT image, OG, non-SDK)
When you need the card as bytes from the bundled art (server/SSR, an NFT image field, an
OG image), use the /node generators:
import { beastCardDataUri, renderBeastCardSvgFromBundle } from "@provable-games/collectables/node";
beastCardDataUri(beast); // "data:image/svg+xml;base64,…" (same shape as token_uri)
renderBeastCardSvgFromBundle(beast); // the raw <svg> string with the bundled art inlinedDeliberate deviation from the chain: the art is drawn with
<image>rather than<foreignObject>, which does not render when an SVG is shown via an<img>element — exactly how apps consume the card.tierColor()/TIER_COLORSare exported too.
Metadata
The API intentionally stores no descriptions or image URIs — the SDK regenerates them,
byte-identical to the on-chain token_uri output:
import {
beastFullName, // '"Ghoul Sun" Lich'
beastTraits, // the exact on-chain 17-trait array
beastTokenMetadata, // full ERC721-style metadata JSON
goldenTokenMetadata,
} from "@provable-games/collectables";
const metadata = beastTokenMetadata(beast, {
imageUrl: beastImageUrl(beast, { baseUrl: "https://yoursite.com/collectables" }),
});Constants and helpers are exported too: BEAST_NAMES, ITEM_NAME_PREFIXES,
ITEM_NAME_SUFFIXES, beastType(), beastTier(), beastPower(), airdropRound(),
normalizeAddress(), and the collection registry (COLLECTIONS).
API
The hosted REST API is public — no key required (cached, rate-limited at 300 req/min/IP).
Point the client elsewhere with new CollectablesClient({ baseUrl }). Endpoints and the
full filter allowlist are discoverable at GET /collections.
License
MIT
