ApeChain API
Public JSON over HTTPS. No key needed.
Quick start
- Base URL
https://explorer.apechain.com- Etherscan-compatible
https://explorer.apechain.com/api- Live blocks (WebSocket)
wss://explorer.apechain.com/ws- Rate limits
- Per IP, per second; higher with an
x-api-keyheader. Every response carriesx-ratelimit-remaining. - OpenAPI
- openapi.json · version 0.1.0
- Etherscan coverage
- Every Etherscan action and how this API answers it
Build with AI
Connect Claude, Cursor or another MCP client to read ApeScan data in plain language. No account or API key is needed.
{
"mcpServers": {
"apescan": {
"url": "https://explorer.apechain.com/mcp"
}
}
}Read-only tools: blocks, transactions, addresses, tokens, NFTs, search, logs, verified contracts and API documentation.
Endpoints
- NFTsItems, owners, activity and sales.11 endpoints
- CollectionsInventory, traits and market data.11 endpoints
- WalletsBalances, history and holdings.14 endpoints
- TokensToken lists, holders and transfers.11 endpoints
- Transactions & blocksBlocks, transactions and receipts.23 endpoints
- Contracts & verificationSource, ABI and verification jobs.9 endpoints
- Tool endpointsLive balances, broadcast, relayer, Vyper.8 endpoints
- Charts & statsDaily series and chain status.4 endpoints
- Search & namesSearch and ENS lookups.4 endpoints
- Etherscan-compatiblemodule and action calls at /api.12 endpoints
First request
One NFT: metadata, owner and traits.
GETItem request
curl -sS 'https://explorer.apechain.com/v1/nfts/0xbebaa24108d6a03c7331464270b95278bbbe6ff7/2202'
Public · ApeChain 33139Open GET ↗
Example response—
Example unavailable. Run the request ↗
GOBS / 2202View this item ↗
Loading example…
Pagination and errors
- Cursors: pass
nextback ascursorwith the same filters. - Coverage: check
coverage.completefor full history. - 400
bad_request: fix the query; do not retry unchanged. - 410
cursor_expired(/v1/nfts/collectionsand/v1/search/tokensonly): restart from the first page. - 429: rate limited; wait for
retry-after. - 500
internal: server error; retry later. - 503
lookup_unavailable: the node is unreachable; retry afterretry-after.
Full client
// Runtime: Browser or Node 18+ JavaScript. Copy this complete artifact into your app.
const DEFAULT_RETRY_AFTER_MS = 2000;
const MAX_RETRY_AFTER_MS = 30000;
function retryAfterMs(response) {
const raw = response.headers.get("retry-after")?.trim() ?? "";
if (!/^\d+(?:\.\d+)?$/.test(raw)) return DEFAULT_RETRY_AFTER_MS;
return Math.min(MAX_RETRY_AFTER_MS, Math.ceil(Number(raw) * 1000));
}
function wait(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
const timer = setTimeout(done, ms);
function done() { signal?.removeEventListener("abort", aborted); resolve(); }
function aborted() { clearTimeout(timer); signal?.removeEventListener("abort", aborted); reject(signal?.reason ?? new DOMException("Aborted", "AbortError")); }
signal?.addEventListener("abort", aborted, { once: true });
});
}
class PaginationError extends Error {
constructor(status, state) { super(`ApeScan ${status}; resume at ${state.url}`); this.status = status; this.state = state; }
}
class PaginationTransportError extends Error {
constructor(state, cause) { super(`ApeScan transport failed; resume at ${state.url}`); this.state = state; this.cause = cause; }
}
class PaginationConsumerError extends Error {
constructor(request, nextUrl, cause) { super("Page consumer failed; committed progress is unknown"); this.request = request; this.nextUrl = nextUrl; this.committed = "unknown"; this.cause = cause; }
}
class PaginationProgressError extends Error {
constructor(reason, request, nextUrl, processedPages) {
super(`Pagination incomplete: ${reason}`); this.reason = reason; this.request = request; this.nextUrl = nextUrl;
this.processedPages = processedPages; this.complete = false; this.committed = reason === "page_limit" ? "processed" : "not_consumed";
this.resumeUrl = reason === "page_limit" ? nextUrl : request.url;
}
}
function validateBounds(maxPages, maxRetries) {
if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 1000 || !Number.isSafeInteger(maxRetries)
|| maxRetries < 0 || !Number.isSafeInteger(maxPages * (maxRetries + 1))) throw new RangeError("Invalid pagination work bounds");
}
function validPage(page) {
if (!page || typeof page !== "object" || Array.isArray(page)) return false;
const d = Object.getOwnPropertyDescriptors(page), items = d.items, next = d.next;
return !!items && Object.hasOwn(items, "value") && Array.isArray(items.value) && !!next && Object.hasOwn(next, "value")
&& (next.value === null || typeof next.value === "string" && next.value.length > 0);
}
function stateFor(url) {
const current = new URL(url);
return { url, cursor: current.searchParams.get("cursor") };
}
function continuationFor(url, cursor) {
if (cursor === null) return null;
const current = new URL(url), query = current.search ? current.search.slice(1).split("&") : [];
const replacement = new URLSearchParams({ cursor }).toString();
const at = query.findIndex((part) => new URLSearchParams(part).has("cursor"));
if (at >= 0) query[at] = replacement; else query.push(replacement);
current.search = query.join("&");
return current.toString();
}
// Default 100 pages, ceiling 1000; safe total attempts <= maxPages*(maxRetries+1).
// This is a local engineering work bound, not a deadline or backend SLA.
async function runPagination({ url, fetcher = fetch, sleep = wait, signal, maxRetries = 3, maxPages = 100, onPage = async () => {} }) {
validateBounds(maxPages, maxRetries);
if (new URL(url).searchParams.getAll("cursor").length > 1) throw new TypeError("Ambiguous initial cursor");
let nextUrl = url;
const requested = new Set();
let processedPages = 0;
while (nextUrl) {
requested.add(stateFor(nextUrl).cursor);
let retries = 0;
for (;;) {
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
const state = stateFor(nextUrl);
let response;
try {
response = await fetcher(nextUrl, { signal });
} catch (cause) {
if (signal?.aborted) throw signal.reason ?? cause;
throw new PaginationTransportError(state, cause);
}
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
if ((response.status === 429 || response.status === 503) && retries++ < maxRetries) {
try { await sleep(retryAfterMs(response), signal); } catch (cause) {
if (signal?.aborted) throw signal.reason ?? cause;
throw cause;
}
continue;
}
if (!response.ok) throw new PaginationError(response.status, state);
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
let page;
try { page = await response.json(); } catch (cause) {
if (signal?.aborted) throw signal.reason ?? cause;
throw new PaginationProgressError("malformed_page", state, null, processedPages);
}
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
if (!validPage(page)) throw new PaginationProgressError("malformed_page", state, null, processedPages);
const continuation = continuationFor(nextUrl, page.next);
if (page.next !== null && requested.has(page.next)) throw new PaginationProgressError("cursor_cycle", state, continuation, processedPages);
try {
await onPage(page, state);
} catch (cause) {
if (signal?.aborted) throw signal.reason ?? cause;
throw new PaginationConsumerError(state, continuation, cause);
}
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
processedPages++;
if (!continuation) return { complete: true, processedPages, resumeUrl: null };
if (processedPages >= maxPages) throw new PaginationProgressError("page_limit", state, continuation, processedPages);
nextUrl = continuation;
break;
}
}
return { complete: true, processedPages, resumeUrl: null };
}
const controller = new AbortController();
const pagination = {
controller,
run: (options = {}) => runPagination({
url: "https://explorer.apechain.com/v1/nfts/collections?limit=100",
signal: controller.signal,
...options,
}),
};
// Example: await pagination.run({ onPage(page, state) { for (const item of page.items) console.log(item); console.info("resume URL:", state.url); } });
// Progress errors include request, nextUrl, processedPages, committed and an explicit resumeUrl.
// A cycle or malformed page is not consumed. Its resumeUrl names the unconsumed request, not a safe automatic retry.
// A page-limit error follows successful consumption and resumes at nextUrl. Never automatically refetch committed pages.
// Call pagination.controller.abort() to cancel fetches and retry waits. Cancellation keeps its original AbortError and has no resume state.