# GraveMint SDK



**`@solanadeads/gravemint`** · version **1.3.0** · install with `npm i @solanadeads/gravemint`

Guide adapted from the published README for clarity. Code examples are preserved.

Every method, with its signature and types: [method reference](https://docs.deads.io/sdk/gravemint-reference.md).




The GraveMint SDK provides collection reads and wallet-signed mint flows for your application. It uses native `fetch`, includes TypeScript declarations and supports ESM and CommonJS. Bring your existing wallet library; no chain SDK is bundled.

The versioned API is additive-only. See [Versioning](#versioning).

```bash
npm install @solanadeads/gravemint
```

Partner keys are invite-only. Request access for your collections and origins; see [Getting a key](#getting-a-key).

---

## Transaction submission {#the-one-rule-worth-reading-first}

`prepare()` builds the transaction. Your wallet signs it, and `execute()` returns the signed bytes to GraveMint for validation and submission.

GraveMint checks the prepared instructions, accounts and amounts before broadcasting. Use the SDK's execute methods rather than broadcasting through your wallet library.

---

## Contents

- [Quick start](#quick-start) · [Getting a key](#getting-a-key) · [Configuration](#configuration)
- [Building a mint page](#building-a-mint-page) — [phases](#phases-and-countdowns) · [price](#price-is-a-shape-not-a-number) · [eligibility](#eligibility) · [capabilities](#capabilities--when-you-cannot-render-a-drop)
- [Minting](#minting) — [the signer](#the-signer) · [wallet-adapter example](#a-real-signer-solanawallet-adapter)
- [Errors and retries](#errors-and-retries) · [Rate limits](#rate-limits)
- [Versioning](#versioning) · [Legacy read API](#legacy-read-api) · [Security](#security-notes-for-partners)

---

## Quick start

```ts
import { GraveMintClient } from "@solanadeads/gravemint";

const gm = new GraveMintClient({ apiKey: "gm_pub_..." });

// 1. Everything needed to render the drop, in one call.
const drop = await gm.v1.collection("deads");

console.log(drop.collection.name);
console.log(drop.stats.mintedCount, "/", drop.stats.totalSupply);

// 2. Which phase is live right now (server-decided, not client-computed).
const phase = drop.phases.active[0];

// 3. Can this wallet mint it?
const verdict = await gm.v1.eligibility(drop.collection.id, phase.id, wallet);

// 4. Mint. You supply the signer; GraveMint broadcasts.
const result = await gm.mint.mint({
  collectionId: drop.collection.id,
  phaseId: phase.id,
  quantity: 1,
  walletAddress: wallet,
  signer,
});
```

---

## Getting a key

Request a partner key for the collections and origins your integration will use.

Choose from three key classes:

| Class | Prefix | Where it belongs | What bounds it |
|---|---|---|---|
| Publishable | `gm_pub_` | your web page — it is *meant* to be visible | the origin allow-list on the key |
| Secret | `gm_live_` | your server only | secrecy — it has no origin binding |
| Sandbox | `gm_test_` | development against test-network drops only | refused on mainnet (`SANDBOX_KEY_ON_MAINNET`); production keys are refused on test networks (`LIVE_KEY_ON_TESTNET`) |

Publishable keys are intended for browser bundles. Register every requesting origin, including staging and previews, and keep the collection scope limited to your integration.

Keep `gm_live_` keys on your server. They do not use browser-origin binding.

Use `gm_live_` for server calls. Browser keys require a matching `Origin` header; a request without one returns `ORIGIN_NOT_ALLOWED`.

---

## Configuration

```ts
new GraveMintClient({
  apiKey:     "gm_pub_...",   // required
  timeoutMs:  30_000,
  maxRetries: 3,              // 429 / 5xx / network
  rateLimit:  { requestsPerSecond: 5, burst: 10 },   // null to disable
  fetch:      globalThis.fetch,
});
```

### Base URLs {#base-urls-—-there-are-two-on-purpose}

The versioned contract is a separate mount from the legacy public read API,
with its own auth and its own middleware. The client keeps two base URLs:

| | default | used by |
|---|---|---|
| `baseUrl` | `https://api.deads.io/gravemint/public` (`api.solanadeads.com` serves the same origin and keeps working) | the [legacy read API](#legacy-read-api) |
| `v1BaseUrl` | `https://api.solanadeads.com/gravemint/v1` | `gm.v1`, `gm.mint` |

You normally set neither. If you set `baseUrl` to something ending in
`/public`, `v1BaseUrl` is derived for you. If it ends in anything else — because
you are routing through your own proxy — the constructor throws and asks you
to pass `v1BaseUrl` explicitly.

Explicit proxy configuration lets the client validate its API routes at startup.

```ts
// Proxying through your own backend:
new GraveMintClient({ apiKey, v1BaseUrl: "https://yoursite.com/api/gravemint" });
```

Node `<18` needs a `fetch` polyfill: `new GraveMintClient({ apiKey, fetch })`.

---

## Building a mint page

Read collection data, phases, eligibility and supported features before enabling minting.

`gm.v1.collection(identifier)` returns all of it in one call. `identifier` is a
`shortId`, a collection UUID, or an on-chain address.

Which one to use:
- Anything a person sees (URLs, share links): the `shortId` or the on-chain address.
- API calls: `drop.collection.id`, the UUID. Every v1 route accepts it, so passing it everywhere is always safe. Until gravemint#4493, some read routes answered a UUID with 404; they now accept it.
- `gm.mint.prepare()` (and `gm.mint.mint()`) also take the `shortId` or on-chain address as `collectionId` (gravemint#4525). It is resolved within your key's own drops only; anything else is refused as not in scope. Pass an address exactly as v1 returned it: it is matched as-is, not case-folded. If two of your drops share an on-chain address, that address resolves to neither, so use the `shortId` or UUID.

```ts
const drop = await gm.v1.collection("deads");

drop.collection    // name, symbol, image, chain, socials, isVerified
drop.stats         // totalSupply, mintedCount, availableCount, percentMinted
drop.phases        // { all, active, upcoming }
drop.capabilities  // can you render this drop faithfully?
drop.serverTime    // our clock — see below
```

### Server-resolved values {#the-rule-that-keeps-you-correct}

Use the returned pricing, eligibility and phase status values. GraveMint evaluates the collection and wallet settings on the server; refresh these reads when the wallet, selected phase or mint state changes.

### Phases and countdowns

```ts
for (const phase of drop.phases.all) {
  phase.status            // "active" | "upcoming" | "ended"
  phase.name
  phase.startDate         // ISO, or null
  phase.endDate
  phase.maxPerWallet      // null = no cap
  phase.maxPerTransaction
  phase.phaseSupply
  phase.isGated           // does it have eligibility requirements?
  phase.bogo              // { buy, get } or null
  phase.msUntilStart      // ← render countdowns from these
  phase.msUntilEnd
}
```

`phases.all` includes ended phases, which you can display in the full schedule or filter from an active view.

Use `msUntilStart` and `msUntilEnd` for countdowns. Anchor a ticking display to `drop.serverTime`, then refresh the collection response when a countdown expires.

### Price is a shape, not a number

```ts
type PriceDisplay =
  | { kind: "amount"; amount: number; currency: string; isFree: boolean; approximate?: boolean }
  | { kind: "range";  min: number; max: number; currency: string }
  | { kind: "hidden" }
  | { kind: "unknown" };
```

Render it by branching, and treat all four as normal:

```tsx
switch (phase.priceDisplay.kind) {
  case "amount":
    return phase.priceDisplay.isFree
      ? <>Free</>
      : <>{phase.priceDisplay.approximate ? "~" : ""}{phase.priceDisplay.amount} {phase.priceDisplay.currency}</>;
  case "range":
    return <>{phase.priceDisplay.min}–{phase.priceDisplay.max} {phase.priceDisplay.currency}</>;
  case "hidden":
    return <>Price revealed when you qualify</>;
  case "unknown":
    return <>Price shown at checkout</>;
}
```

`priceDisplay` represents the price available for display before a mint is prepared. It accounts for the phase's pricing settings. Hidden prices remain hidden until the wallet qualifies; unknown prices have no display amount for the request.

Show “Free” only for an amount with `isFree: true`. Do not replace hidden or unknown values with zero.

The final wallet-specific breakdown is returned by `prepare()` as `pricing`, including applicable benefits and fees.

### Eligibility

```ts
const v = await gm.v1.eligibility(collectionId, phaseId, walletAddress);

v.gated              // does this phase gate at all?
v.meetsRequirements  // the verdict
v.onAllowlist
v.spotsAllocated
v.spotsRemaining
v.requirements       // [{ type, met, description }] — render these
v.phase              // { id, name, hasStarted, hasEnded }
v.message
```

This is server-evaluated because it has to be: the dominant gate is an
NFT-holding check needing an on-chain lookup, and an allowlist must never be
published to a client.

`meetsRequirements` reports wallet requirements separately from the time window. Check `phase.hasStarted` and `phase.hasEnded` as well before enabling minting. For ungated phases, use `gated: false` as the requirements check.

```ts
const canMintNow = v.meetsRequirements && v.phase.hasStarted && !v.phase.hasEnded;
```

### The rest of the drop — art, feed, bounty, per-wallet counts

These are all scoped to your key the same way `collection()` is: a key issued for
another drop gets `403 COLLECTION_NOT_IN_SCOPE`, never an empty list.

```ts
await gm.v1.gallery("deads", { limit: 24 });   // NFTs still available to mint
await gm.v1.recentlyMinted("deads");           // the live "just minted" feed
await gm.v1.bounty("deads");                   // bounty summary, when there is one
await gm.v1.bountyPrizes("deads");             // its public prize table
await gm.v1.walletMints("deads", wallet);      // "you have minted 2 of 3", per phase
```

`walletMints` includes bonus mints such as BOGO and bounty mints. Eligibility uses separate allocation rules. Use the wallet-mint response when displaying minted or remaining counts.

### Claim codes

Claim-code endpoints describe the collection’s code configuration and the benefits a wallet has already redeemed.

```ts
await gm.v1.claimCodes(collectionId);                          // does this drop use codes?
await gm.v1.claimCodeBenefits(collectionId, wallet);           // what has this wallet earned?
await gm.v1.validateClaimCode(collectionId, 'CODE-123');       // { valid, grantType, ... }
```

`validateClaimCode` checks a code without redeeming it or consuming a use. A code outside the collection returns the same response as an unknown code.

`prepare` applies benefits already redeemed by the minting wallet for this collection. It does not accept a claim code. The collector redeems codes on the hosted GraveMint page, where the required checks and signature are handled.

To display benefits before minting, call `claimCodeBenefits(collectionId, wallet, { phaseId })`. Include the selected phase and, for a pack drop, `packTierId` to restrict the calculation appropriately. The `prepare` response's `pricing` remains the final amount.

### Pricing helpers

```ts
await gm.v1.tokenPrices();            // platform token prices, for an SPL-priced drop
await gm.v1.peggedPrice(phaseId);     // resolved amount for a USD/native-pegged phase
await gm.v1.dutchPrice(phaseId);      // live price of a dynamic Dutch phase
```

Render `phase.priceDisplay` — it already resolves Dutch and pegged prices (a pegged
figure comes back `approximate`). v1 phases carry no raw `price` field; these two
helpers are for refreshing a live number without re-reading the whole drop.

Both phase routes are scoped through the phase's own collection: a phase id is not a way
around collection scope.

### Supply, traits and social proof

```ts
await gm.v1.availability(collectionId);   // approximate supply counts — a progress bar, never a sold-out decision
await gm.v1.traits(identifier);           // trait names and values, for gallery filters
await gm.v1.allMinted(identifier, { limit: 100, offset: 0 });  // ONE page (default 20, max 100) — page with offset
await gm.v1.topHolders(identifier);       // largest holders
await gm.v1.topMinters(identifier);       // who minted the most
```

### Capabilities — when you cannot render a drop

```ts
drop.capabilities  // { requiresFeatures: string[], supportedBySurface: boolean, mintUrl: string | null }
```

v1 covers core mint: supply, phases, fixed and pegged pricing (including BOGO
and hidden prices), claim-code benefits already redeemed, eligibility, per-wallet
limits, and minting on Solana. `requiresFeatures` names what a drop needs beyond that;
any of these makes `supportedBySurface` false:

| Feature | Why the drop is not core-renderable |
|---|---|
| `packs` | a phase sells pack tiers |
| `gallery_mode` | the collector chooses which NFT to mint |
| `nft_selection` | a phase restricts which NFTs can be minted (by rarity, trait, or a specific list) |
| `generative` | a generative collection — traits are composed at mint and the price carries a surcharge |
| `dutch_auction`, `dynamic_dutch` | the live price decays or surges, so `priceDisplay` is a range or `unknown`, not a price tag |
| `cross_chain` | payment on another chain, which partner surfaces never support |
| `non_solana_chain` | the drop is not on Solana — v1 mints Solana only |
| `claim_codes` | the drop has active claim codes (a collection-level fact — codes are redeemed against the drop) |

This list only grows. Branch on `supportedBySurface`, not on the names.

```tsx
if (!drop.capabilities.supportedBySurface) {
  return <a href={drop.capabilities.mintUrl}>Mint on GraveMint</a>;
}
```

Handle unsupported capabilities before enabling minting, so collectors can use the hosted page for features outside your integration.

Collections with unrecognized requirements are marked unsupported. Use `mintUrl` when provided to open the hosted collection page.

---

## Minting

```ts
const prepared = await gm.mint.prepare({
  collectionId, phaseId, quantity: 1, walletAddress,
  affiliateCode,        // optional; validated server-side, ignored if unused
  walletProvider: wallet.adapter.name, // optional; fills your wallet breakdown, else "Unknown"
});

const signed = await signer.signTransaction(prepared.transactions[0].transaction);

const result = await gm.mint.execute({
  sessionId: prepared.sessions[0].sessionId,
  signedTransaction: signed,
});
```

`prepare` returns `transactions[]` and `sessions[]`, including for quantity one. Read `sessions[0].sessionId` for the first transaction. `mint()` handles the pairing for you.

Or in one call:

```ts
const result = await gm.mint.mint({
  collectionId, phaseId, quantity: 1, walletAddress, signer,
});
```

`prepare()` does the real work — eligibility, limits, pricing, supply
reservation, transaction build — and returns `pricing` (the authoritative
breakdown), `sessions[]` (one per transaction), `expiresAt` / `expiresInMs`, and
`quantityAdjusted` — true when fewer NFTs were prepared than requested (a wallet or
phase limit, or supply). Tell the collector before they sign; `adjustmentReason` says why.

`execute` waits up to 130 seconds by default — longer than the server's own budget,
which can still be broadcasting after a shorter client gives up — unless you set a
client-wide `timeoutMs`, which is respected. It is never retried automatically:
re-sending signed transactions gets `ALREADY_PROCESSING` / `ALREADY_COMPLETED` back,
which would hide whether the first attempt landed.

If `gm.mint.mint()` loses the answer AFTER the signed transaction left your client — a
timeout, a dropped connection, any 5xx (including an edge timeout such as Cloudflare's
524), the server's own `408`, or `ALREADY_PROCESSING` / `ALREADY_COMPLETED` /
`RECONCILE_PENDING` / `MINT_PAYMENT_UNVERIFIED` — it throws
`MintOutcomeUnknownError` (`code: "OUTCOME_UNKNOWN"`, `sessionIds`). The mint may
still land. Do not retry: a retry prepares again and can mint twice. Show its
message ("check your wallet before trying again"). A 5xx refused *before* broadcast
looks the same from here, so it is reported as unknown too — the safe direction.

`mint()` also *returns* (does not throw) when a batch confirmed only some, or none, of
its transactions: `{ success: false, partial, message }`. Anything but `success: true`
is not a success.

Sessions expire. Do not hold a signature and submit it later; prepare again.
A prepared session also reserves supply, so do not call `prepare()` just to read
a price — that is what `priceDisplay` is for.

### The signer

```ts
interface TransactionSigner {
  signTransaction(base64Transaction: string): Promise<string>;
}
```

The signer accepts a base64 transaction and returns the signed transaction in base64. You can also supply `signAllTransactions` to request batch approval.

### Wallet-adapter example {#a-real-signer-solana-wallet-adapter}

```ts
import { VersionedTransaction } from "@solana/web3.js";

const signer = {
  async signTransaction(base64: string) {
    const tx = VersionedTransaction.deserialize(Buffer.from(base64, "base64"));
    const signed = await wallet.signTransaction(tx);
    return Buffer.from(signed.serialize()).toString("base64");
  },
};
```

For legacy transaction support and browser Buffer setup, see [the signer adapter](https://docs.deads.io/guides/signing.md#solana-wallet-adapter).

### Chain support

The v1 mint flow supports Solana. Check `capabilities.supportedBySurface` before preparation and offer `mintUrl` for collections that require another flow.

---

## Errors and retries

Every error carries a machine-readable `code`, a human `message` safe to show a
collector, plus `.status` and `.requestId` for support.

```ts
import { GraveMintError, RateLimitError } from "@solanadeads/gravemint";

try {
  await gm.mint.mint({ ... });
} catch (err) {
  if (err instanceof GraveMintError) {
    console.error(err.code, err.requestId);
    showToast(err.message);      // safe: no internals, no schema details
  }
}
```

### Error handling {#🚨-read-this-before-branching-on-a-code}

The API's stable v1 error catalog covers authentication, scope and collection lookups. Mint handlers and middleware can return additional codes, including `NO_NFTS_AVAILABLE`, `PHASE_ENDED`, `WALLET_LIMIT_EXCEEDED`, `SESSION_NOT_FOUND`, `INSUFFICIENT_FUNDS` and `NOT_WHITELISTED`.

Handle known codes explicitly and keep a message fallback for additional or code-less errors.

For batch outcomes, read each transaction's `errorCode` from the returned `results` array. A batch HTTP response can succeed even when individual transactions fail or remain pending.

```ts
import { isBatchMintResult } from "@solanadeads/gravemint";

const res = await gm.mint.mint({ ... });
if (isBatchMintResult(res) && res.success === false) {
  // res.partial: did SOME land? res.minted of res.requested; res.message is safe to show.
  for (const r of res.results) {
    if (!r.success) console.warn(r.sessionId, r.errorCode);  // e.g. TX_MODIFIED
  }
}
```

Branch on documented stable codes where your application needs specific recovery behavior. Display the returned message for other responses.

```ts
const code = err instanceof GraveMintError ? err.code : undefined;
if (code === "TX_MODIFIED") { /* never retry — see below */ }
else if (code === "ORIGIN_NOT_ALLOWED") { /* config: send us the origin */ }
else { showToast(err.message); }   // covers the unfrozen and code-less cases
```

### Stable v1 codes {#frozen-—-v1-s-own-codes}

| Code | Meaning | Retry? |
|---|---|---|
| `COLLECTION_NOT_FOUND` | no such drop, or not publicly servable | no |
| `LOOKUP_FAILED` | the collection lookup could not complete | yes, backoff |
| `COLLECTION_REQUIRED` | the request named no collection | no — fix the call |
| `CODE_REQUIRED` | a claim-code check arrived with no code | no — fix the call |
| `API_KEY_REQUIRED` / `API_KEY_INVALID` | missing or wrong key | no — fix config |
| `API_KEY_INACTIVE` / `API_KEY_EXPIRED` | key revoked or lapsed | no — talk to us |
| `ORIGIN_NOT_ALLOWED` | valid key, unregistered origin (or a `gm_pub_` key used server-side) | no |
| `COLLECTION_NOT_IN_SCOPE` | valid key, not issued for this drop | no |
| `AUTH_UNAVAILABLE` | the key check could not complete | yes, backoff |

Also frozen, and emitted by the shared mint handlers: `COLLECTION_CLOSED` ·
`NFTS_LOCKED` · `SESSION_EXPIRED` · `TX_MODIFIED`.

For single mints, a transaction failure is exposed as a thrown error code. Batch mint outcomes use `results[].errorCode`; inspect those returned results as well as thrown errors.

Use server submission for all v1 mints. The following codes require changing the request rather than resubmitting it:

- `CLIENT_BROADCAST_NOT_ALLOWED` — the request carried `transactionHash`
  (or `txHash` / `transaction_hash`). Sign the transaction `prepare-mint`
  returned and send it back as `signedTransaction`; we submit it and confirm it.
- `CHAIN_NOT_SUPPORTED_BY_SURFACE` — the drop is on a chain this API does not
  serve. v1 is Solana-only. This is distinct from `CHAIN_DISABLED`, which
  means a chain was retired platform-wide; your drop's chain is fine, it is just
  out of scope here. Use `mintUrl` from the collection response to send the
  collector to GraveMint instead.

Frozen but not emitted on the v1 path: `CHAIN_DISABLED` ·
`UNSUPPORTED_BY_SURFACE` (not emitted anywhere yet), and `NOT_ELIGIBLE` ·
`WALLET_MISMATCH` (emitted elsewhere in GraveMint, on the cross-chain and
mint-page-token paths — neither of which v1 mounts). They will not be renamed.

### The eligibility endpoint

`gm.v1.eligibility()` reuses the first-party handler, so it has its own two
codes. Neither is frozen:

| Code | Meaning | Retry? |
|---|---|---|
| `INVALID_ADDRESS` | the wallet address is not valid for this chain | no — fix the call |
| `CHECK_BUSY` | the eligibility engine is at its global budget | yes, short backoff |

### Rate limiting and suspension

| Code | Meaning | Retry? |
|---|---|---|
| `RATE_LIMITED` | too many requests for this key | yes, honour `Retry-After` |
| `COLLECTION_RATE_LIMITED` | too many prepares against this collection, from everyone | yes, honour `retryAfter` |
| `WALLET_BLOCKED` | this wallet is suspended | no — on `prepare-mint` the body also carries `reason` / `blockedUntil` / `permanent`; on `execute-mint` it is the code and message only |
| `WALLET_HELD` | a wallet this request is made by or for — the signed-in wallet, a minting or recipient wallet named in the body, or the wallet behind the mint session — is on a security hold (reported compromised) | no — show the message; the collector must contact SolanaDeads |
| `WALLET_HOLD_UNVERIFIED` | holds are active and we could not confirm the wallet behind this request (typically: a mint session's wallet could not be read), so we refused rather than guess (HTTP 503) | yes, backoff |
| `ASSET_BANNED` | this collection or token is on the platform ban list | no |

### Bot protection and the server's time budget

Every v1 request passes the platform's bot protection and a per-request time budget. None of
these is fixed by retrying the same request immediately:

| Code | Meaning | Retry? |
|---|---|---|
| `BOT_BLOCKED` | this client was blocked by bot protection | no |
| `BOTNET_DETECTED` | the request's signature (e.g. its User-Agent) matches known automation tooling | no — from a server, send a descriptive `User-Agent` |
| `WALLET_BOT_BLOCKED` | the wallet in this request is restricted by bot protection | no |
| `BOT_DETECTED` | the request scored as automated | no — back off |
| `REQUEST_TIMEOUT` (HTTP 408) | the server's time budget ran out — the work may still finish | reads: yes, backoff. `execute-mint`: never — `mint()` reports it as `OUTCOME_UNKNOWN` |

### Key class and scope

A key carries a class (live or sandbox) and a list of scopes. These are refusals of the
key for *this* request, not of the drop, so none of them is fixed by retrying:

| Code | Meaning | Retry? |
|---|---|---|
| `SCOPE_NOT_GRANTED` | the key is valid but was not issued the scope this call needs (e.g. `mint`) | no — ask us to add the scope |
| `SANDBOX_KEY_ON_MAINNET` | a sandbox (`gm_test_`) key was used against a mainnet drop | no — use a production key: `gm_pub_` in a browser, `gm_live_` on a server |
| `LIVE_KEY_ON_TESTNET` | a production key (`gm_pub_` or `gm_live_`) was used against a test-network drop | no — use your sandbox (`gm_test_`) key |

These codes distinguish test-network and production credentials.

`TX_MODIFIED` means the signed transaction differs from the prepared transaction. Check the adapter for changed instructions or accounts, then prepare a new transaction. Do not resend the modified bytes.

`ORIGIN_NOT_ALLOWED` requires origin registration; `API_KEY_REQUIRED` indicates a missing credential.

The SDK retries `429`/`5xx`/network automatically — except `execute`, which it
never replays (see "Minting") — with exponential backoff,
honouring `Retry-After`, up to `maxRetries`. It does not auto-retry the
terminal codes above.

## Rate limits

| Limit | Value | Scope |
|---|---|---|
| Reads on `/v1` (shared with `/gravemint/public`) | 600 / minute | per IP |
| `/v1/prepare-mint` and `/v1/execute-mint` (shared bucket) | 60 / minute | per key |
| `prepare-mint`, per COLLECTION | 30 / 10s | per collection, shared with first-party traffic |
| `validateClaimCode` (claim-code check) | 20 / minute | per IP, shared with first-party code checks |
| SDK client-side limiter | 5 rps sustained, 10 burst | your process |

Account for each limit when sizing request queues. A shared limit can apply before a per-key limit is reached.

Two sharing details that matter in practice:

- `prepare` and `execute` share the same 60/min bucket, and a mint needs one
  of each — so the sustained ceiling is roughly 30 mints per minute per key,
  not 60.
- The read limiter is the same instance `/gravemint/public` uses, so v1 reads
  and first-party public reads from one IP draw on one 600/min bucket.
- The per-collection prepare cap is shared with gravemint.io. It is keyed
  on the collection alone, so on a hot drop your prepares compete with every
  first-party collector's — and on a busy launch it can bind well below the
  60/min-per-key figure. Treat 60/min as a ceiling you may not reach, not a
  reservation.

The mint limit is keyed on the API key, not the IP, so a partner proxying every
collector through one server is not sharing a bucket with the rest of the
internet — but it does mean all of that partner's collectors share it.

- The claim-code check is per IP, not per key. Validate from the collector's
  browser. A partner that validates from its own server (a `gm_live_` key) sends every
  collector's check from one address, so all of its users share 20 a minute.

On a `429` the SDK honours `Retry-After` — or, when a limiter sends it only in the
body, `retryAfter` — and retries up to `maxRetries`. The server body is on the thrown
error as `err.details`. Disable
the client-side limiter with `rateLimit: null` only if you have your own queue.
Contact us if your integration needs a higher limit.

---

## Versioning

`v1` is additive-only. Within the major we will not remove a field, change a
field's type, rename an error code, or tighten validation. New behaviour arrives
as new fields or a new version. Unversioned legacy paths keep working.

Pin the package version in your application and use capability checks to handle collections that require newer or additional features.

```ts
await gm.v1.version();   // { version: "v1", stability: "additive-only", docs }
```

`x-gm-client` identifies the SDK version on each request, including browser requests.

Deprecations will be announced with a migration path and a real timeline, and
old majors keep working while both are live.

---

## Legacy read API

The original read-only surface is unchanged and still supported:

| Resource | Methods |
|---|---|
| `gm.discovery` | `getFeatured`, `getLiveMints`, `getTrending`, `getUpcoming`, `getRecentSoldouts`, `listCollections`, `searchCollections` |
| `gm.collections` | `get`, `getGallery`, `getRecentlyMinted` |
| `gm.nfts` | `get` |
| `gm.bounty` | `getSummary`, `getPrizes` |
| `gm.platform` | `getCapabilities`, `getTokenPrices` |
| `gm.codes` | `validate` (read-only — does not redeem) |

These legacy resources use the first-party public API and are not available to partner keys. Use the scoped `gm.v1` methods for partner collection reads and `gm.mint` for minting.

Cross-collection discovery remains first-party. The v1 equivalents for collection data include `collection`, `eligibility`, `gallery`, `recentlyMinted`, `bounty`, `bountyPrizes` and `walletMints`.

Use `gm.v1.collection()` for display prices and capability checks.

---

## Security notes for partners

- Register the exact origins used by your browser application, including staging and previews.
- Keep `gm_live_` keys on your server and request revocation if a key is exposed.
- Use the API's resolved pricing and eligibility results.
- Return signed transactions to GraveMint for submission.

---

## Development

```bash
npm test          # unit tests, no network
npm run typecheck
npm run build     # dual ESM + CJS
```

## License

MIT

---
Source: https://docs.deads.io/sdk/gravemint
Markdown: https://docs.deads.io/sdk/gravemint.md
