GraveMint v1 API
Base URL — https://api.solanadeads.com/gravemint/v1
Every example on this page is real
These are actual responses recorded from production, not illustrations. If a field appears here it exists; if a status code appears here it is one the API really returns. Regenerate with npm run sync.
Authentication
Two ways in, and exactly one of them is right for your situation.
| header | bounded by | use it | |
|---|---|---|---|
| Publishable | X-API-Key: gm_pub_… | the origin allow-list on the key | in your web page |
| Secret | X-API-Key: gm_live_… | secrecy | from your server |
A publishable key is public by construction — it ships in your bundle and anyone can read it. What protects it is that it only works from origins registered on the key. So the job is not hiding it, it is telling us every origin you serve from, staging and preview domains included.
Never put a gm_live_ key in browser code
It has no origin binding, so a leak is unbounded until we revoke it.
The converse also bites: a gm_pub_ key does not work server-side. It is admitted by matching the request's Origin against the key's list, and a Node process sends no Origin — so it is refused every time, with no way to satisfy it.
Try it live
Every endpoint below is a working request builder. Fill in the parameters, press Send, and you get both halves of the exchange side by side — the exact request that left your browser, and exactly what came back, with status and timing.
Why every console here is a DEVNET drop
Each console runs against 7zn5qa (DEAD DAWGS — ONCHAIN TEST), our own mpl-core drop on solana-devnet, using a gm_test_ sandbox key. Three reasons, all measured rather than cautious:
- Nothing here can touch a real mint. A
gm_test_key is refused on any production chain (403 SANDBOX_KEY_ON_MAINNET), server-side. That is what makes a live prepare-mint console publishable at all. - The examples stay working. A production drop decays under the documentation. These examples were previously captured from a mainnet drop, and by the time anyone looked it read
percentMinted: 100— the reference example was a sold-out drop. - The data is better. That mainnet drop's gallery returned
total: 0, so the gallery console demonstrated nothing. This one has 867 items.
Publishable keys are public by construction — they ship in a partner's bundle and anyone can read them. What bounds this one is server-side: an origin allow-list (only docs.solanadeads.com), a collection scope (this one drop), and a chain restriction (devnet only). Secrecy is not what protects it, so printing it costs nothing.
Your own integration would use a gm_pub_ key — same origin-binding and scoping, issued for your drop. See Keys and origins.
execute-mint is shown, not sent
prepare-mint below does send — safely, because it targets the devnet drop with the sandbox key, and because the balance pre-flight runs before the NFT lock, so the default request reserves nothing.
execute-mint cannot send, and not for a safety reason: it needs signed bytes from a wallet, which a documentation page has no way to produce. Its builder shows exactly what to send.
Discovery
/No parameters. Confirms the surface and its stability contract.
Sent
GET https://api.solanadeads.com/gravemint/v1/
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/'Received
Read a drop
Address a drop by its short id or its on-chain address — never an internal uuid, which is not a public identifier and is refused.
/collections/{identifier}Everything needed to build a mint page, in one call.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa'Received
Where are the sales phases?
Inside that same response — there is deliberately no separate phases endpoint. /collections/{identifier} returns phases: { active, upcoming, all }, each with its window, supply, per-wallet cap, gating flag and resolved priceDisplay, alongside serverTime so a countdown does not drift.
That is the whole point of the read model: live drops average five phases (max 38), so a page needs the phase list, the active one and the next transition together. Splitting them across calls would mean rendering a page from several inconsistent snapshots.
Press Send on the console above and look for phases in the response.
Do not compute the active phase yourself
There are three notions of "active" in GraveMint's schema and they do not agree. v1 exposes exactly one — enabled plus the date window — and status on each phase 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.
Check eligibility
Server-side verdict for one wallet against one phase. Gating criteria stay on our side — you receive the decision, never the allowlist.
/collections/{collectionId}/eligibility/{phaseId}/{walletAddress}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'.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/eligibility/8426923e-0d64-480d-925f-90324c2dedf5/11111111111111111111111111111111
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/eligibility/8426923e-0d64-480d-925f-90324c2dedf5/11111111111111111111111111111111'Received
The rest of the drop
Everything else a mint page is built from, scoped to the key the same way.
Two consoles here return an error on purpose
The key these consoles use is issued for one drop. Put deads in any Collection box and you get 403 COLLECTION_NOT_IN_SCOPE — that is the confinement working, not a fault. Likewise the uuid option above returns 404, because an internal id is not a public identifier.
Every console's default values return 200.
/collections/{identifier}/galleryThe drop's art. 867 items here, so paging is real.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/gallery?limit=3
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/gallery?limit=3'Received
/collections/{identifier}/recently-mintedThe live 'just minted' feed.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/recently-minted?limit=3
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/recently-minted?limit=3'Received
/collections/{collectionId}/wallet-mints/{walletAddress}How many this wallet minted and how many remain, per phase. Do NOT derive this from eligibility: it INCLUDES bonus mints (BOGO, bounty) and the eligibility engine excludes them, so a figure you compute yourself is wrong for any wallet that took one.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/wallet-mints/11111111111111111111111111111111
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/wallet-mints/11111111111111111111111111111111'Received
/collections/{identifier}/bountyBounty summary. This drop has none configured, so you get the empty shape — which is the response you have to handle, not an error.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/bounty
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/bounty'Received
/collections/{identifier}/bounty-prizesThe public prize table. Returns enabled: false when there is no bounty.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/bounty-prizes
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/bounty-prizes'Received
Claim codes
17.6% of live drops use them, so a mint page that ignores claim codes cannot render one in six drops correctly.
/collections/{collectionId}/claim-codesDoes this drop use codes, and of what kind?
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/claim-codes
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/claim-codes'Received
/collections/{collectionId}/claim-codes/benefits/{walletAddress}What a wallet is already entitled to from codes it has redeemed.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/claim-codes/benefits/11111111111111111111111111111111
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/claim-codes/benefits/11111111111111111111111111111111'Received
/collections/{collectionId}/claim-codes/validateREAD-ONLY — checking a code does not redeem it or consume a use.
Sent
POST https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/claim-codes/validate
X-API-Key: gm_test_47716d…
Content-Type: application/json
{
"code": "TRY-A-CODE"
}as curl
curl -s -X POST \
-H 'X-API-Key: gm_test_47716d…' \
-H 'Content-Type: application/json' \
-d '{"code":"TRY-A-CODE"}' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/claim-codes/validate'Received
Pricing
Never render phase.price for a pegged phase
It is Dutch-resolved but not peg-resolved, so on a pegged phase it is a stale cached token amount. One live phase carries 110000 TOUCHGRASS in that field. Use these.
/token-pricesPlatform token prices — what an SPL-priced drop is worth in USD. Global: it names no collection, so it carries no drop data and needs no collection scope.
Sent
GET https://api.solanadeads.com/gravemint/v1/token-prices
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/token-prices'Received
/phases/{phaseId}/pegged-priceThe resolved amount for a USD- or native-pegged phase. Scoped through the phase's own collection — a phase id is not a way around collection scope.
Sent
GET https://api.solanadeads.com/gravemint/v1/phases/8426923e-0d64-480d-925f-90324c2dedf5/pegged-price
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/phases/8426923e-0d64-480d-925f-90324c2dedf5/pegged-price'Received
/phases/{phaseId}/dutch-priceThe live price of a dynamic Dutch phase, which moves with time.
Sent
GET https://api.solanadeads.com/gravemint/v1/phases/8426923e-0d64-480d-925f-90324c2dedf5/dutch-price
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/phases/8426923e-0d64-480d-925f-90324c2dedf5/dutch-price'Received
Supply, traits and social proof
/collections/{collectionId}/availabilityHow much is actually mintable right now, accounting for held reservations.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/availability
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/availability'Received
/collections/{identifier}/traitsTrait names and values, for filtering a gallery.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/traits
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/traits'Received
/collections/{identifier}/all-mintedEvery minted item in the drop.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/all-minted
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/all-minted'Received
/collections/{identifier}/top-holdersLargest holders.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/top-holders
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/top-holders'Received
/collections/{identifier}/top-mintersWho minted the most.
Sent
GET https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/top-minters
X-API-Key: gm_test_47716d…as curl
curl -s -X GET \
-H 'X-API-Key: gm_test_47716d…' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/top-minters'Received
What your key can reach
Every route that names a collection is scoped to the collections your key was issued for. Measured against production with a key scoped to one drop, asking for another:
| Request | Result |
|---|---|
GET /collections/{other} | 403 COLLECTION_NOT_IN_SCOPE |
GET /collections/{other}/gallery | 403 |
GET /collections/{other}/recently-minted | 403 |
GET /collections/{other}/bounty · /bounty-prizes | 403 |
GET /collections/{other}/wallet-mints/{wallet} | 403 |
GET /collections/{other}/eligibility/{phase}/{wallet} | 403 |
POST /prepare-mint with another drop's id | 403 |
GET / (version — names no collection) | 200 |
The refusal is deliberately identical whether or not the drop exists, so an unauthorised caller learns only that this key cannot read it — never whether the collection is real.
Two further bounds apply on top of scope: a key with an origin list is refused from any other origin (403 ORIGIN_NOT_ALLOWED, including a request with no Origin at all, which is why a browser key cannot be used server-side), and a sandbox key is refused on any production chain (403 SANDBOX_KEY_ON_MAINNET).
Prepare a mint
This one really sends, against the same devnet drop with the sandbox key.
With the default wallet you get 400 INSUFFICIENT_BALANCE — and that is the point: the balance pre-flight runs before the NFT lock, so the default request is a complete round-trip that reserves nothing. Click it as often as you like. Paste a funded devnet wallet to see real transactions and sessions come back; that request does take a three-minute lock on devnet supply, which is what prepare is for.
/prepare-mintReturns transactions for your signer. You sign them; WE broadcast.
Sent
POST https://api.solanadeads.com/gravemint/v1/prepare-mint
X-API-Key: gm_test_47716d…
Content-Type: application/json
{
"collectionId": "2e239364-fc9b-4191-ba62-b57dbac76394",
"phaseId": "8426923e-0d64-480d-925f-90324c2dedf5",
"walletAddress": "11111111111111111111111111111111",
"quantity": "1"
}as curl
curl -s -X POST \
-H 'X-API-Key: gm_test_47716d…' \
-H 'Content-Type: application/json' \
-d '{"collectionId":"2e239364-fc9b-4191-ba62-b57dbac76394","phaseId":"8426923e-0d64-480d-925f-90324c2dedf5","walletAddress":"11111111111111111111111111111111","quantity":"1"}' \
'https://api.solanadeads.com/gravemint/v1/prepare-mint'Received
Execute a mint
/execute-mintnot sentSend the SIGNED BYTES. A transactionHash here is refused with CLIENT_BROADCAST_NOT_ALLOWED.
Sent
POST https://api.solanadeads.com/gravemint/v1/execute-mint
X-API-Key: gm_test_47716d…
Content-Type: application/json
{
"sessionId": "<from prepare-mint>",
"signedTransaction": "<base64 signed tx>"
}as curl
curl -s -X POST \
-H 'X-API-Key: gm_test_47716d…' \
-H 'Content-Type: application/json' \
-d '{"sessionId":"<from prepare-mint>","signedTransaction":"<base64 signed tx>"}' \
'https://api.solanadeads.com/gravemint/v1/execute-mint'Received
The console below issues a real request from your browser to the production API, using a publishable key scoped to one collection we own.
Why we can print a key on a public page
gm_pub_ keys are public by construction — they ship in a partner's bundle and anyone can read them. What bounds this one is the origin allow-list on the key (it only works from docs.solanadeads.com) and its collection scope (it can read exactly one drop, ours). Secrecy is not what protects a publishable key, so printing it costs nothing.
Reads only, and that is deliberate
There is no prepare-mint button here. prepare takes a 3-minute NFT lock and reserves against the BOGO and claim-code ledgers, so a public "Try it" on it would let anyone grief a live drop's supply. A devnet sandbox key is the right way to make the full mint flow explorable, and is tracked separately.
Discovery
Confirms the surface and its stability contract.
curl -s -H 'X-API-Key: gm_pub_your_key' \
'https://api.solanadeads.com/gravemint/v1/'{
"success": true,
"version": "v1",
"stability": "additive-only",
"docs": "https://www.npmjs.com/package/@solanadeads/gravemint"
}Reading a drop
One call returns everything needed to build a mint page: the collection, every phase with its resolved price, live supply, and our clock for countdowns.
Address a drop by its short id or on-chain address — never a uuid
/v1/collections/deads and /v1/collections/<collection_address> work. The internal database id is not a public identifier and is not accepted.
GET /v1/collections/:identifier
curl -s -H 'X-API-Key: gm_pub_your_key' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa'{
"success": true,
"collection": {
"id": "2e239364-fc9b-4191-ba62-b57dbac76394",
"shortId": "7zn5qa",
"name": "DEAD DAWGS - ONCHAIN TEST",
"symbol": "ONCHAIN1",
"description": "Testing",
"image": "https://gravemint-storage.s3.us-east-1.amazonaws.com/launchpad-collection-images/1774886146380/collection.png",
"bannerImage": "https://gravemint-storage.s3.us-east-1.amazonaws.com/launchpad-collection-images/1774886157110/banner.png",
"chain": "solana-devnet",
"collectionAddress": "7mS96x56GoeE23R8DRXYusjecFY9udEw2NTnedqJ1Ed5",
"contractAddress": null,
"externalUrl": null,
"twitter": null,
"discord": null,
"isVerified": false
},
"stats": {
"totalSupply": 888,
"mintedCount": 21,
"availableCount": 867,
"percentMinted": 2.364864864864865
},
"phases": {
"all": [
{
"id": "8426923e-0d64-480d-925f-90324c2dedf5",
"name": "Public",
"status": "active",
"startDate": "2026-03-30T16:02:00.000Z",
"endDate": null,
"priceDisplay": {
"kind": "amount",
"amount": 1,
"currency": "USD",
"isFree": false
},
"bogo": null,
"isGated": false,
"maxPerWallet": 100,
"maxPerTransaction": 10,
"phaseSupply": null,
"msUntilStart": null,
"msUntilEnd": null
}
],
"active": [
{
"id": "8426923e-0d64-480d-925f-90324c2dedf5",
"name": "Public",
"status": "active",
"startDate": "2026-03-30T16:02:00.000Z",
"endDate": null,
"priceDisplay": {
"kind": "amount",
"amount": 1,
"currency": "USD",
"isFree": false
},
"bogo": null,
"isGated": false,
"maxPerWallet": 100,
"maxPerTransaction": 10,
"phaseSupply": null,
"msUntilStart": null,
"msUntilEnd": null
}
],
"upcoming": []
},
"capabilities": {
"requiresFeatures": [
"claim_codes",
"nft_selection"
],
"supportedBySurface": false,
"mintUrl": "https://gravemint.io/mint/7zn5qa"
},
"serverTime": "2026-08-31T12:17:45.816Z"
}Price is a shape, not a number
priceDisplay is a resolved value, computed by the same code that prices the real mint. Do not reassemble it from raw fields — the cascade has several stages that overwrite each other, and the last one that applies decides the charge.
kind | meaning | render |
|---|---|---|
amount | a settled figure | the number and its currency |
range | varies within the phase | "from X" |
hidden | the creator chose to hide it until eligible | do not guess — say it is hidden |
unknown | genuinely not computable for an anonymous caller | do not render 0 |
Never render 0 as "Free"
unknown and hidden are not zero. A drop that shows "Free" because a price could not be resolved is the failure this shape exists to prevent.
Eligibility
Server-side verdict for one wallet against one phase. Gating criteria stay on our side; you receive the decision, never the allowlist.
GET /v1/collections/:collectionId/eligibility/:phaseId/:wallet
curl -s -H 'X-API-Key: gm_pub_your_key' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa/eligibility/8426923e-0d64-480d-925f-90324c2dedf5/11111111111111111111111111111111'{
"success": true,
"gated": false,
"phase": {
"id": "8426923e-0d64-480d-925f-90324c2dedf5",
"name": "Public",
"startDate": "2026-03-30T16:02:00+00:00",
"endDate": null,
"hasStarted": true,
"hasEnded": false
},
"address": "11111111111111111111111111111111",
"message": "This phase is open to everyone — no allowlist or holder requirements."
}Errors
Coded, and the code is the part you branch on. The human sentence may be reworded at any time; the code will not.
No credential at all
No Origin, no key.
curl -s \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa'{
"success": false,
"error": "This request could not be authorised.",
"code": "API_KEY_REQUIRED"
}An origin we do not recognise
Distinguished from the above on purpose — this one is a configuration fix on our side, and conflating the two turns a five-minute answer into a support thread.
curl -s -H 'Origin: https://partner.example' \
'https://api.solanadeads.com/gravemint/v1/collections/7zn5qa'{
"success": false,
"error": "This request could not be authorised.",
"code": "ORIGIN_NOT_ALLOWED"
}| code | status | meaning |
|---|---|---|
API_KEY_REQUIRED | 401 | no key and no recognised origin |
API_KEY_INVALID | 401 | the key did not match a live record |
ORIGIN_NOT_ALLOWED | 403 | the origin is not on this key |
COLLECTION_NOT_IN_SCOPE | 403 | this key is not issued for that drop |
AUTH_UNAVAILABLE | 503 | we could not check — retry; never treat as denial |
CLIENT_BROADCAST_NOT_ALLOWED | 400 | you sent a transaction hash. See below |
CHAIN_NOT_SUPPORTED_BY_SURFACE | 400 | v1 mints Solana only |
The one rule
You sign. We broadcast.
prepare-mint returns transactions. Your signer signs them. The signed bytes come back to us and we submit them.
Never broadcast a mint transaction yourself. /v1/execute-mint refuses a request carrying transactionHash with CLIENT_BROADCAST_NOT_ALLOWED, and there is no flag to turn that off — it is what lets us verify that what reaches the chain is what we priced and reserved.
