Collection page
A working collection page: header stats, an item grid with listing prices, and a live activity feed. No credential required — GraveMarket's read API is public.
Check it before you ship it
With the MCP server installed, ask your agent to "review this file for Solana Deads integration mistakes".
Install
bash
npm install @solanadeads/gravemarketts
// lib/gravemarket.ts
import { GraveyardClient } from '@solanadeads/gravemarket';
export const gmk = new GraveyardClient();Nothing to configure. No key, no origin registration, no scoping.
The page
tsx
import { useEffect, useState } from 'react';
import { gmk } from './lib/gravemarket';
// listing_price is NULLABLE and null means NOT LISTED — not free, not zero.
function priceLabel(item) {
return item.listing_price == null
? 'Not listed'
: `${item.listing_price} ${item.listing_currency}`;
}
// token_id is null on Solana, so key on token_address alone.
const itemKey = (i) => i.token_address;
export function CollectionPage({ slug }: { slug: string }) {
const [collection, setCollection] = useState(null);
const [stats, setStats] = useState(null);
const [items, setItems] = useState([]);
const [activity, setActivity] = useState([]);
const [cursor, setCursor] = useState(undefined);
const [loading, setLoading] = useState(false);
useEffect(() => {
gmk.collections.get(slug).then(setCollection);
gmk.collections.stats(slug).then(setStats);
gmk.collections.activity(slug, { limit: 10 }).then((r) => setActivity(r.data));
loadMore(true);
}, [slug]);
async function loadMore(reset = false) {
setLoading(true);
const page = await gmk.collections.items(slug, {
limit: 24,
...(reset ? {} : { cursor }),
});
setItems((prev) => (reset ? page.data : [...prev, ...page.data]));
setCursor(page.cursor); // null/undefined when exhausted
setLoading(false);
}
if (!collection) return <p>Loading…</p>;
return (
<div>
<header>
<h1>{collection.name}</h1>
{/* Floor currency varies BY CHAIN — always render it alongside the number. */}
<p>
Floor: {collection.floor_price ?? '—'} {collection.floor_currency ?? ''}
{' · '}{collection.chain}
</p>
{stats?.wash_trade_count_7d > 0 && (
<p>
Volume figures include {stats.wash_trade_count_7d} suspected wash
trade(s) in the last 7 days.
</p>
)}
</header>
<ul>
{items.map((item) => (
<li key={itemKey(item)}>
<img src={item.thumbnail_url ?? item.image_url} alt={item.name ?? ''} />
<span>{item.name ?? itemKey(item).slice(0, 8)}</span>
<span>{priceLabel(item)}</span>
{item.rarity_rank && <span>Rank #{item.rarity_rank}</span>}
</li>
))}
</ul>
{/* `cursor` present means more pages. Do not compute totals from what is loaded. */}
{cursor && (
<button onClick={() => loadMore()} disabled={loading}>
{loading ? 'Loading…' : 'Load more'}
</button>
)}
<section>
<h2>Recent activity</h2>
{activity.map((e, i) => (
<div key={i}>{e.event_type} — {e.price ?? '—'} {e.currency ?? ''}</div>
))}
</section>
</div>
);
}What this deliberately does not do
- No
items.lengthas a total. That is a page count, not a collection size — the collection record carries the real one. Deriving a total from a partial page is the failure mode cursors invite. - No cross-chain price sort.
listing_currencyvaries by chain, so ranking a mixed result set compares SOL to ETH. - No bare price render.
listing_price == nullis not listed, and rendering it as0says "free". - No composite
address:tokenIdkey.token_idis null on Solana.
Each has a failure mode behind it — see the marketplace model.
Draining every page
For a real total — volume, a full activity export — drain the cursor. The SDK has generators for the two big ones:
ts
const events = [];
for await (const batch of gmk.collections.activityAll(slug)) events.push(...batch);There is no generator for holders, so drain that one by hand:
ts
const holders = [];
let cursor;
do {
const page = await gmk.collections.holders(slug, { limit: 100, cursor });
holders.push(...page.holders);
cursor = page.cursor;
} while (cursor);Note holders comes back under holders, not data — the list endpoints are not all shaped identically, so read the response rather than assuming.
Next
- The marketplace model — the rules above, with the why
- GraveMarket v1 API — live consoles for every endpoint
- React mint panel — the GraveMint side, if you also render a mint
