Gram Forge
Token metrics, the full holder list and Gram DNS resolution, served from an index we keep warm. No signup, no key, no CORS restrictions.
This API is in beta. Everything documented here works and is serving live data — but field names and response shapes can still change while the ecosystem settles. Breaking changes, incidents and maintenance are announced in @GrmAPI_Bot before they happen — subscribe if you are building on this. For anything else, @nosint.
Request examples
Real responses from this deployment. Pick a group, open the link, compare it with what you get back.
Supply, holder count, mintability, price and market cap. One call for a dashboard header.
Run it{
"ok": true,
"data": {
"symbol": "GRM",
"name": "Grm",
"decimals": 9,
"master": {
"raw": "0:b8ef4f77a17e5785bd31ba4da50abd91852f2b8febee97ad6ee16d941f939198",
"bounceable": "EQC47093oX5Xhb0xuk2lCr2RhS8rj-vul61u4W2UH5ORmG_O",
"non_bounceable": "UQC47093oX5Xhb0xuk2lCr2RhS8rj-vul61u4W2UH5ORmDIL"
},
"description": "The first-ever PoW jetton on TON Blockchain.",
"image": "https://gramcoin.org/img/icon.png",
"total_supply": 2434215638.26197,
"holders": 92963,
"mintable": true,
"price_usd": 0.0007634,
"market_cap_usd": 1858280.218
}
}
Reference
All read-only, all returning the same envelope. Tap any path to run it.
The full holder index, filterable.
Resolution, browsing and artwork.
Paging & filtering
Every list endpoint takes the same shape of parameters. Two of them decide whether your integration stays fast at scale.
/v1/holders| Parameter | Values | Notes |
|---|---|---|
| limit | 1–1000, default 50 | Rows per page. |
| cursor | next_cursor from the previous response |
The fast path. Costs the same on page 10 000 as on page 1. Pass it back unmodified. |
| offset | 0–10 000 | Simple but linear: the database walks every preceding row. Rejected above 10 000 — use the cursor. |
| min_balance | GRM, e.g. 100000 | Only holders at or above this balance. |
| max_balance | GRM | Combine with min_balance for a band. |
| with_domain | true |
Only holders who own a .gram name. The name is on every row as domain; when someone owns several, the shortest is shown. |
| order | desc (default), asc |
By balance. asc starts at the dust. |
/v1/pools/{address}/providers| Parameter | Values | Notes |
|---|---|---|
| limit | 1–1000, default 50 | Providers per page, largest share first. |
| offset | 0–100 000 | The set is small enough that offset is fine here. |
On both STON.fi and DeDust a share of a pool is an LP jetton whose master
contract is the pool itself, so providers are the holders of that jetton.
Reading it that way uses one source we already trust rather than two
exchange APIs with different shapes and limits. share is the
fraction of total LP supply; lp_balance is the raw amount.
The set is replaced wholesale on each sync, because membership changes as
people withdraw and upserting would leave them listed forever.
GRM DEX's own GRM/USDT pool isn't listed on any price aggregator, so its
numbers are read from the chain directly instead of from a feed.
price_usd, price_native and change_24h
come from the pool contract's own reserves. volume_24h and
txns_24h come from reconstructing real swaps out of the
pool's transfer history — matched transfer pairs sharing one
counterparty — rather than from reserve differences, which undercounts
when trades land close together. Either figure carries
"volume_estimated": true only when the walk hits its length
cap with history still unread, not as a standing caveat.
/v1/dns| Parameter | Values | Notes |
|---|---|---|
| limit | 1–500, default 50 | Rows per page. |
| offset | 0–100 000 | The collection is small enough that offset is fine here. |
| q | substring, e.g. need |
Matches anywhere in the name. Trigram-indexed, so it stays fast. |
| owner | any address form | Everything one account holds. Raw, EQ… and UQ… all work. |
| on_sale | true | Only names currently listed on a marketplace. |
| sort | index (default), name, price, expiry |
price puts the cheapest listing first; expiry the soonest to lapse. |
/v1/dns-batch| Parameter | Values | Notes |
|---|---|---|
| names | comma-separated, up to 25 | Each resolved exactly as /v1/dns/{name} would — same fields, same cache. |
| fresh | 1 |
Key holders only, same restriction as the single-name endpoint. Forces every name in the batch past the cache at once, so it costs more upstream calls than the single form multiplied by the batch size — use it sparingly. |
Returns {"results": [{"requested": "...", "domain": {...}}, ...]},
one entry per name in the order given, so the response lines up with the request
without matching on the resolved name. An entry that could not resolve carries
"error" instead of "domain" — a typo in one of twenty-five
names doesn't fail the other twenty-four. GET, not POST: this API has no write
endpoints, and a comma-separated list needs none either.
/v1/dns-expiring| Parameter | Values | Notes |
|---|---|---|
| days | 1–365, default 30 | Rent due inside this many days from now. |
| symbols | length in characters | Exact, not a maximum — symbols=1 means k.gram,
l.gram, never ab.gram. |
| limit | 1–500, default 100 | Rows per page. |
| offset | 0–100 000 |
A read from the index the bulk sync already keeps current, not a resolution — nothing here reaches the chain, so it costs the same whether you ask about one day or a year. Sorted soonest-first. A name with no owner has no rent clock and never appears here regardless of the filters.
| Field | Meaning |
|---|---|
| mint_price_grm | What the name went for when it was first claimed, in GRM. |
| mint_bids | Bids in that first round. One means it was bought outright; more means it was auctioned. |
| minted_by | The wallet that paid. For a fixed-price mint this is followed back past the collection. |
| mint_fee_gram | Network coin spent on the transaction. A fee, not a price — note the different suffix. |
| last_price_grm | Present only when the name lapsed and was won again. What the current owner paid. |
| auctions | Every round, oldest first, with its price, bid count, winner and date. |
| settled | Whether the winner was confirmed as an owner. False means unverified, not disproved. |
A name can be won more than once: at the original mint, and again every time
it expires and goes back up. tele.gram was won for 160 811 GRM
in 2024, lapsed, and was won again for 583 332 in 2025 — so a single
"price" would answer the wrong question whichever number it carried.
Old mints may have no price at all: resolution needs transfer history, and
the indexer does not keep it forever. Missing means null.
Request a page, read next_cursor, pass it as cursor on the
next request, stop when it comes back null. Never increment
offset in a loop — that is the slow path by construction.
?min_balance=1000&with_domain=true&limit=100 is one query, one
index scan. total counts rows matching the filters, not the whole table.
rank is populated when paging by offset. With a cursor it is
omitted, because computing it would need the same full scan the cursor exists to
avoid. Use /v1/holders/{address} for one exact rank.
/v1/dns?on_sale=true&sort=price&limit=20. Prices are nanograms in
price_nano; divide by 10⁹ for GRM.
Conventions
Six things that will save you an afternoon.
Success is {"ok":true,"data":…}, failure is
{"ok":false,"error":"code","message":"…"}. Status codes always agree
with ok.
Every address is returned as raw, bounceable and
non_bounceable. Any of the three is accepted as input, so you never
convert anything.
Mint dates, expiry and prices come from resolving a domain, which reaches
the chain. A list returns what the index already holds, so some rows carry
more than others. /v1/dns/{name} resolves one domain fully —
lists do not, because a wallet can hold dozens of names and each
resolution is about ten upstream calls.
A figure we could not determine is null, never 0.
A zero here is a real zero. Fields that cannot apply — auction details on
an owned name — are absent rather than null, so a null always
means "unknown" and never "not relevant".
Every timestamp is ISO-8601 in UTC with a Unix integer beside it
(expires_at_unix). expires_in_days truncates — use
expires_in_seconds when hours matter.
GRAM is the network's native coin, formerly Toncoin. GRM is this jetton,
contract EQC47093…mG_O. Fields ending _gram are the coin;
_usd is dollars.
Holders, the catalogue, pools and distribution are read from our own index, refreshed every ten minutes. A single domain and a wallet's names are reconciled against the chain once a minute, so what you see is at most a minute behind a transfer. Mint prices and auction history never change once known.
Concurrent lookups for one domain collapse into a single trip to the chain — the cost of this API scales with how many different things you ask about, not with how often. Polling one name every second costs no more than polling it once a minute.
Every response carries X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Policy, so you
can pace without discovering the ceiling by hitting it. Going too fast is
answered with 429 and a Retry-After — never a
503, which would claim the service failed when it did not.
X-API-Stage: beta on everything, and /v1/health
reports the running build, the stage and where changes are announced. A client
that never opens this page still sees it.
@GrmAPI_Bot posts breaking changes, incidents and maintenance windows — and nothing else. If a field is going to move, you hear it there first. Quiet weeks stay quiet.
A field that looks wrong, a case this doesn't cover, an integration you're planning, or a heads-up before something changes — all of it goes to @nosint. While this is in beta that is the only channel, and it is a fast one.
Respect Cache-Control and you will never hit the rate limit.
| Data | Fresh for | Why |
|---|---|---|
| /v1/price | 10 s | Read live from the pools |
| /v1/pools | 15 s | Same source, heavier payload |
| /v1/token | 20 s | Supply moves slowly |
| /v1/holders | 30 s | Served from our index |
| /v1/dns/{name} | 3 min | Four chain calls per resolution |
| images | 1 year | Deterministic — a name always renders the same |
The holder and domain indexes refresh in the background; every holder response carries
synced_at so you can see exactly how current it is.