React mint panel
A working panel that handles every case the read model can return. Roughly 90 lines, no GraveMint-specific state management, and nothing recomputed client-side.
Check it before you ship it
If you have the MCP server installed, ask your agent to "review this file for Solana Deads integration mistakes" — it checks for the specific things this page is careful about.
Install
npm install @solanadeads/gravemintThe client
Create it once, outside your component tree.
// lib/gravemint.ts
import { GraveMintClient } from '@solanadeads/gravemint';
export const gm = new GraveMintClient({
apiKey: import.meta.env.VITE_GRAVEMINT_KEY, // a gm_pub_ key — origin-bound
});Only a gm_pub_ key belongs in browser code
Anything with a public build prefix (VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_) is inlined into the client bundle by definition. A gm_live_ key there is a full partner credential published to every visitor, and nothing bounds it to an origin.
The panel
import { useEffect, useState } from 'react';
import { useWallet } from '@solana/wallet-adapter-react';
import { VersionedTransaction } from '@solana/web3.js';
import { gm } from './lib/gravemint';
function priceLabel(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 '—';
}
}
export function MintPanel({ identifier }: { identifier: string }) {
const { publicKey, signTransaction } = useWallet();
const [drop, setDrop] = useState(null);
const [verdict, setVerdict] = useState(null);
const [status, setStatus] = useState('');
useEffect(() => { gm.v1.collection(identifier).then(setDrop); }, [identifier]);
const phase = drop?.phases?.active;
const wallet = publicKey?.toBase58();
// Eligibility is a SERVER verdict — never inferred from holdings you fetched yourself.
useEffect(() => {
if (!drop || !phase || !wallet) return;
gm.v1.eligibility(drop.collection.id, phase.id, wallet).then(setVerdict);
}, [drop, phase, wallet]);
if (!drop) return <p>Loading…</p>;
// Always handle this branch: it is what keeps a pinned client honest when a drop
// uses a mechanic this surface does not model.
if (!drop.capabilities.supportedBySurface) {
return <a href={drop.capabilities.mintUrl}>Mint on GraveMint</a>;
}
if (!phase) return <p>No active phase.</p>;
const canMint =
verdict?.meetsRequirements && verdict.phase.hasStarted && !verdict.phase.hasEnded;
async function mint() {
try {
setStatus('Preparing…');
const prep = await gm.mint.prepare({
collectionId: drop.collection.id,
phaseId: phase.id,
walletAddress: wallet,
quantity: 1,
});
setStatus('Approve in your wallet…');
const tx = VersionedTransaction.deserialize(
Buffer.from(prep.transactions[0], 'base64'),
);
const signed = await signTransaction(tx);
// We broadcast. Never send a transaction hash.
setStatus('Minting…');
await gm.mint.execute({
sessionId: prep.sessions[0],
signedTransaction: Buffer.from(signed.serialize()).toString('base64'),
});
setStatus('Minted.');
} catch (err) {
// A batch keeps its codes inside results[]; a single mint promotes them.
const code = err.code ?? err.results?.find((r) => r.errorCode)?.errorCode;
setStatus(code === 'TX_MODIFIED' ? 'Something altered the transaction. Start over.'
: code === 'SESSION_EXPIRED' ? 'That took too long — try again.'
: err.message);
}
}
return (
<div>
<h2>{drop.collection.name}</h2>
<p>{drop.stats.minted} / {drop.stats.totalSupply} minted</p>
<p>{priceLabel(phase.priceDisplay)}</p>
{!wallet && <p>Connect a wallet to continue.</p>}
{wallet && !canMint && <p>{verdict?.reason ?? 'Not eligible for this phase.'}</p>}
<button onClick={mint} disabled={!canMint}>Mint</button>
<p>{status}</p>
</div>
);
}What this deliberately does not do
- No price arithmetic.
priceLabelreads the union and nothing else. - No client-side eligibility. The verdict is the server's.
- No broadcast. It signs and hands the bytes back.
- No
Date.now()countdown. If you add one, anchor it todrop.serverTime.
Each of those is a rule with a failure mode behind it — see the read model and Wallets and signing.
Next
- Troubleshooting — what each error actually means
- v1 API reference — every endpoint, with live consoles
