The read model
A mint page is mostly reading. Which phase is live, what it costs, whether this wallet qualifies, how many they may take, how long is left. Minting is the last step and the smallest one.
gm.v1.collection(identifier) returns all of it in one call. That is deliberate: live drops average five phases (max 38), and a page assembled from several calls is a page assembled from several inconsistent snapshots.
const drop = await gm.v1.collection('your-drop');
// drop.collection — name, images, chain, socials
// drop.stats — minted / total / available / percent
// drop.phases — { active, upcoming, all }
// drop.capabilities— { requiresFeatures, supportedBySurface, mintUrl }
// drop.serverTime — anchor your countdown to this, not Date.now()Phases
Every phase carries its window, name, supply, per-wallet cap, gating flag and a resolved priceDisplay.
Do not compute the active phase yourself
There are three notions of "active" in GraveMint's schema and they do not agree — enabled + date window, an is_active column, and a legacy view. v1 exposes exactly one (enabled + window), and each phase's status is the server's verdict.
Phase transitions are driven by server-side timers, not lazily on read, so a client cannot derive the next one either. Poll, or subscribe.
phases.all reflects what the server considers servable. If you want to show a countdown to the next phase, read upcoming and anchor to serverTime.
Pricing — a union, never a number
This is the single most important thing on this page.
type PriceDisplay =
| { kind: 'amount'; amount: number; currency: string; isFree: boolean }
| { kind: 'range'; min: number; max: number; currency: string }
| { kind: 'hidden' }
| { kind: 'unknown' };All four are normal, not error states:
| kind | when | render |
|---|---|---|
amount | a settled price | the number and its currency |
range | a Dutch auction between bounds | "0.45 – 1.2 SOL" |
hidden | the creator conceals the price until a wallet qualifies — ~10% of live phases | "Price revealed when you qualify" |
unknown | genuinely not computable for an anonymous caller | "—" |
function render(p) {
switch (p.kind) {
case 'amount': return p.isFree ? 'Free' : `${p.amount} ${p.currency}`;
case 'range': return `${p.min} – ${p.max} ${p.currency}`;
case 'hidden': return 'Price revealed when you qualify';
default: return '—';
}
}Never reassemble a price yourself
GraveMint's cascade has ten stages where later ones overwrite earlier ones, and six of them are invisible to any public read — pack tiers, bounty free mints, claim-code benefits, XP vouchers, generative base fees, and the fee/rent terms. A price you compute client-side will disagree with what the collector is charged.
Rendering 0 as "Free" is the specific bug the resolver exists to prevent.
⚠️ phase.price is Dutch-resolved but NOT peg-resolved. On a pegged phase it is a stale cached token amount — one live phase carries 110000 TOUCHGRASS in that field. Use peggedPrice(phaseId) for those.
Eligibility
const v = await gm.v1.eligibility(collectionId, phaseId, wallet);Evaluated server-side by necessity: the dominant gate is an on-chain NFT holding check, and an allowlist must never be published to a client.
The verdict deliberately excludes the time window
It answers "will this wallet qualify", with phase.hasStarted / hasEnded reported separately — so an upcoming phase does not read as "not eligible".
const canMintNow = v.meetsRequirements && v.phase.hasStarted && !v.phase.hasEnded;Per-wallet limits
const mints = await gm.v1.walletMints(collectionId, wallet);Do not derive this from the eligibility verdict
They count differently on purpose: walletMints includes bonus mints (BOGO, bounty) and the eligibility engine excludes them. For any wallet that took one, a figure you compute yourself is wrong in the direction that lets a collector believe they have mints left — and then get refused at prepare.
Capabilities — degrading honestly
Additive-only versioning stops a pinned client from crashing. It does not stop one being subtly wrong, which is worse: a panel that looks fine and misstates the deal.
if (!drop.capabilities.supportedBySurface) {
return <a href={drop.capabilities.mintUrl}>Mint on GraveMint</a>;
}requiresFeatures names what the drop actually needs. Packs, gallery selection and generative builders are outside v1's core-mint scope, so a drop built around them degrades to the fallback rather than rendering a wrong panel.
Always handle this branch. It is the mechanism that keeps an old integration honest.
What this buys you
Every number here is a server verdict. The SDK's job is to transport and type them, not to re-derive them — which is what makes a partner integration correct by construction rather than by discipline.
