Skip to content
IN THIS GUIDE

Collection page ​

This example combines collection stats, an item grid and recent activity. Adapt it to your application's loading, error and refresh behavior. GraveMarket's public reads do not require a credential.

Integration review

With the MCP server installed, ask your agent to "review this file for Solana Deads integration mistakes".

Install ​

bash
npm install @solanadeads/gravemarket
ts
// lib/gravemarket.ts
import { GraveyardClient } from '@solanadeads/gravemarket';
export const gmk = new GraveyardClient();

The public client works without a key. See optional API keys for authenticated use.

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}`;
}

// Include the chain and token ID for EVM collections.
const itemKey = (i) => `${i.chain}:${i.token_address}:${i.token_id ?? ''}`;

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 ?? item.token_address.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>
  );
}

Adapting the example ​

Add request cancellation or stale-response guards when the selected collection changes, and display request failures with a retry action. This example fetches recent activity once; poll or refresh it if your UI needs continuing updates.

Use the collection record for total supply, preserve currency labels and show “Not listed” for a missing listing price. An item key must account for the chain and, on EVM, the token ID.

Draining every page ​

For a full activity export, follow all pages. The SDK provides async generators:

ts
const events = [];
for await (const batch of gmk.collections.activityAll(slug)) events.push(...batch);

For holders, follow the cursor explicitly:

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);

The holders response uses holders for its list field. Check each endpoint's response type when adding pagination.

Next ​