Developers — Polkadex Mainnet Explorer API
Public read-only JSON API for the Polkadex Mainnet (a Polkadot-SDK / Substrate Layer-1). Used by this explorer and freely consumable by external apps, native mobile clients, servers, and AI assistants.
Start here — use the JSON API, not the HTML
This explorer is a client-rendered single-page app: HTML pages are a shell that JavaScript fills in inside the browser. A non-browser client that fetches an HTML page will not see the data. Don't scrape the HTML — call the JSON API below, which returns plain JSON. Every figure on the site comes from an /api/* endpoint.
Two things trip up automated clients, and how to handle them:
| Behavior | What to do |
|---|---|
| The site sits behind Cloudflare. Requests from cloud/datacenter IP ranges are sometimes challenged, so a call from a CI runner or hosted backend can come back empty where the same call from a laptop succeeds. | It's an edge policy, not an API restriction — a plain curl gets HTTP 200. Send a descriptive User-Agent, respect the Cache-Control headers, and ask the operator to allowlist your range if you're calling from a data centre. |
The API itself is open to non-browser clients. CORS is a browser-only mechanism, so a caller that sends no Origin header (native app, server, script, AI agent) is always allowed. | Call the API directly from servers and native apps with no configuration. Only browser callers from other web origins need to be added to ALLOWED_ORIGINS. |
A concise machine-readable index of the whole API — including the exact /api/network-info schema — lives at /llms.txt.
CORS & non-browser clients
The CORS policy in server.js allows three caller categories:
| Caller | Why it works |
|---|---|
| Native mobile apps (iOS, Android, React Native — anything not running inside a browser) | CORS is a browser-only mechanism; native HTTP clients don't send an Origin header, so the server's "if no Origin, allow" branch fires. |
| Server-side proxies (your backend calling ours) | Same — no Origin header. |
Web apps at origins listed in the ALLOWED_ORIGINS env var (defaults to explorer.polkadex.ee + localhost:3000) | Explicitly allowed. |
A web app at a different origin will be blocked by the browser's CORS check until its origin is added to ALLOWED_ORIGINS (operator change, requires a backend restart). Native mobile apps need no configuration at all.
Caching tiers
| Tier | Endpoints | Cache-Control |
|---|---|---|
| Short | High-velocity feeds (/api/blocks, /api/transactions, /api/events, /api/council, /api/governance/latest, /api/state/*) | public, max-age=5, s-maxage=10, stale-while-revalidate=30 |
| Medium | Slower-moving aggregates (/api/validators, /api/network-info, /api/holders, /api/price-latest, /api/discussions, /api/analytics/*) | public, max-age=30, s-maxage=60, stale-while-revalidate=120 |
| Long | Historical series and metadata (/api/price-history, /api/treasury, /api/democracy, /api/governance/calendar, /api/rpc/metadata) | public, max-age=300, s-maxage=600, stale-while-revalidate=3600 |
| No shared cache | Per-caller responses. A missing header is an omission, not permission — do not reuse these (/api/wallet/:address, /api/identity/:address, /api/account/:address, /api/staking-rewards/:address, /api/search/:query, /api/extrinsic-by-hash/:txHash, /api/transactions/older, /api/labels/*, POST /api/rpc/call, /api/auth/*, /api/email/*) | no-store, or no header at all |
Don't poll faster than max-age.
Chain data (read-only, public)
GET /api/blocks— most recent blocksGET /api/block/:number— single block with extrinsics + eventsGET /api/events— most recent on-chain eventsGET /api/transactions— most recent transactions. balances.Transfer ONLY — deposits, withdrawals, treasury payouts and vesting releases are real balance changes that never appear here; use /api/events for every movementGET /api/transactions/older?before=<n>— pagination further back. Echo resumeInBlock back as ?resumeBlock=&resumeCount= when resumeRequired is true, or you will re-read that height foreverGET /api/extrinsic/:block/:txHash— single-extrinsic detailGET /api/extrinsic-by-hash/:txHash— find which block holds a tx hash by scanning backwards from the head. ?recent=<blocks> (default 200, max 2000). found:false means "not in the scanned window", NOT "does not exist"GET /api/validators— full validator set with stake + commissionGET /api/validator/:address— per-validator era historyGET /api/holders— top-balance accountsGET /api/account/:address— account-level summaryGET /api/network-info— network metrics (schema below)GET /api/search/:query— LIVE-RPC block-number / block-hash / account lookup. Does not search the SQLite index and does not resolve extrinsic hashes — use /api/extrinsic-by-hash/:txHash for thoseGET /api/staking-rewards/:address— per-address reward historyGET /api/staking-rewards-status— reward-backfill progressGET /api/wallet/:address— wallet dashboard payload (balances, staking incl. activeStakedPlanck as a u128 string, unpaid rewards, recent activity)
Chain inspection (read-only, polkadot.js-style)
Generic access to runtime metadata, storage and constants at any block — the endpoints behind /chain-state. Backed by an archive node, so historical queries work.
GET /api/rpc/metadata— every pallet with its storage items (key arity + types), constants, calls, events, errorsGET /api/state/:pallet/:item— read any storage item. ?args= keys, ?at= block number/hash, ?entries=1 to list a map (capped)GET /api/consts/:pallet/:item— runtime constants; ?at= supportedGET /api/runtime— runtime spec/impl version; ?at= supportedGET /api/decode/:block— every extrinsic decoded argument by argument (name, type, human, JSON, raw hex). Filters ?section= ?method= ?index=; matching ignores case and underscoresPOST /api/rpc/call— allowlisted read-only RPC methods
Read-only by construction. Only api.query, api.consts and allowlisted read RPCs are reachable — nothing here can submit an extrinsic.
Send storage keys as strings. A u64 key such as 9223372036854775808 (2⁶³) is past JavaScript's MAX_SAFE_INTEGER, so a client that parses it as a number silently queries 9223372036854776000 — a different key whose empty result looks like confirmation. Responses echo args back so you can verify nothing was coerced.
Check hex, not human. toHuman() abbreviates hashes (an all-zero H256 renders as 0x0000…0000, indistinguishable from a mostly-zero one) and group-separates large integers. Every response therefore carries human, JSON and hex together, plus a count for Vec-valued results.
Note that ValueQuery maps return a default-constructed value for an absent key, so isEmpty can be false for a key that was never set — judge emptiness from the decoded contents.
curl 'https://explorer.polkadex.ee/api/decode/12250870?method=submit_snapshot'
curl 'https://explorer.polkadex.ee/api/state/ocex/validatorSetId?at=12250870'
curl 'https://explorer.polkadex.ee/api/state/ocex/authorities?args=6280&at=12250870'
Schema — GET /api/network-info
The home-page network panel in one call. Top-level response:
{
"networkInfo": {
"activeEra": number, // current staking era index
"avgValidatorCommission": number, // mean active-validator commission, %
"avgApy": number, // headline AVG APY %, commission-adjusted
"avg_apy": number, // snake_case alias of avgApy
"validators": { "active": number, "total": number },
"nominators": { "active": number, "total": number },
"maxActiveStake": number, // largest active-validator total stake, PDEX
"minStake": number, // minimum active stake, PDEX
"averageStake": number, // mean active-validator stake, PDEX
"avgStakePerAccount": number, // total bonded / staking accounts, PDEX
"totalIssuance": number, // total PDEX issuance
"totalBonding": number, // total PDEX bonded for staking
"totalBondingPercent": number, // totalBonding / totalIssuance, %
"totalUnbonding": number, // total PDEX currently unbonding
"totalStakeChange": number, // net stake change vs previous era, PDEX
"lastEraRewardsTotal": number // total rewards paid last era, PDEX
},
"lastSync": number, // epoch ms when networkInfo was computed
"status": "Synced" | "Stale" | "Initializing" | "Error",
"chainHead": {
"value": number, // best block number
"lastAdvanceAt": number, // epoch ms the head last advanced
"staleSeconds": number, // seconds since the head last advanced
"isStale": boolean // true if the head looks stuck
}
}
AVG APY is now returned directly (avgApy, and the avg_apy alias) so you don't have to recompute it. It's derived as avgApy = 23.09 × (1 − avgValidatorCommission / 100), where 23.09% is the chain's nominal maximum APY at its target staking ratio.
Account inspection (read-only, RPC-backed)
GET /api/identity/:address— on-chain identity, following identity.superOf → identityOf so sub-accounts resolve as "Parent / Sub"GET /api/proxies/:address— proxy delegations declared by the addressGET /api/proxy-types— the runtime's ProxyType enum variants, read out of chain metadata rather than hardcodedGET /api/multisigs/:address— pending multisig calls for the address
Each returns HTTP 501 if this runtime does not have the corresponding pallet.
Community labels
GET /api/labels/:address— labels with score, up/down votes, reportCount, vetoed, createdAt, plus topLabel. With a session the payload also carries the caller’s own viewerVote, so only the anonymous response is cacheablePOST /api/labels/:address— add or replace your own label for that addressDELETE /api/labels/:address— remove your own labelPOST /api/labels/:address/:signer/vote— up/down-vote someone else's labelPOST /api/labels/:address/:signer/report— report a label (self-reports are rejected 400)POST /api/labels/:address/:signer/veto— veto a label on YOUR OWN address (the caller must be :address)
The GET is public. Every write needs a wallet-signed session (see Authenticated) and answers 401 without one.
Analytics
GET /api/analytics/timeseries?days=N— daily series, N clamped to 1–365. The 7/30/90/365 windows are pre-warmed by the indexer; other windows aggregate liveGET /api/analytics/snapshot— current KPI rollup (issuance, bonded, validator/nominator counts, indexed row counts)
Price feed
GET /api/price-latest— current PDEX price, last-sync, and a bySource map with one entry per configured provider (coingecko live by default)GET /api/price-history?days=N— daily price series (N capped at 4000); each row tagged with the source that supplied it
Providers are pluggable via the PRICE_PROVIDERS env var (csv; default coingecko, a keyless public API).
CoinGecko is keyless (optional COINGECKO_API_KEY for higher limits); CoinMarketCap needs CMC_API_KEY.
Governance
GET /api/council— council members, motions, runners-upGET /api/treasury— treasury balance + proposals (open + historical)GET /api/democracy— referenda + public proposalsGET /api/governance/latest— most-recent OPEN referendum / proposal (drives the homepage banner)GET /api/governance/calendar— unified timeline across referenda, motions and treasury
Email alerts
POST /api/email/subscribe— double opt-in signup (rate-limited per IP)GET /api/email/confirm?token=<t>— renders a confirmation page with a button (read-only)POST /api/email/confirm— performs the confirmation, token in a form field (audit F-001/F-036)GET /api/email/unsubscribe?token=<t>— renders an unsubscribe page with a button (read-only)POST /api/email/unsubscribe— performs the unsubscribe; also the RFC 8058 List-Unsubscribe-Post targetGET /api/email/preferences?token=<t>— fetch current event preferencesPOST /api/email/preferences— update preferences (token in body). Backs the /email/preferences page linked from every alert email; that route is noindex because the token is the credential
Discussions
GET /api/discussions— discussion threads attached to governance itemsGET /api/discussions/:id— single thread with posts
Authenticated
POST /api/auth/challenge— request a sign-in noncePOST /api/auth/verify— submit { address, signature } — the nonce is NOT sent, the server looks up the open challenge for that address; receive { token, expiresIn } in msPOST /api/auth/logout— invalidate the session tokenPOST /api/discussions/:id/posts— post to a discussion (rate-limited, requires a session)
Wallet-signed nonce login. Sessions are 192-bit random tokens with a 7-day TTL; /api/auth/verify returns that TTL as expiresIn (ms). Audit F-152: this said "a TTL" while the UI said ~24 hours and the server used 7 days — three numbers, none of them checkable against another.
Build provenance & liveness
GET /api/version— { component, gitSha, builtAt, dirty, startedAt, specVersion, rpcConnected }. dirty:true means the image was built from a tree with uncommitted changes, so gitSha alone does not identify the running code. Node version, pid and raw uptime are deliberately not publishedGET /version.json— the frontend's equivalent, served as a static fileGET /api/health— { healthy: <bool> } — true only when the chain RPC is up. No credential required; this is the endpoint uptime monitors should watch
Operator-only diagnostics live under /api/diag/* behind a token sent in an Authorization: Bearer or X-Diag-Token header, or a loopback source address. They are not part of the public contract and are not listed here.
Errors
Most failures return a 4xx/5xx status with { "error": "<message>" }. The error string is written for display — do not match on it; it is reworded freely between releases.
RPC-dependent endpoints surface 503 during chain RPC outages, carrying a stable code alongside the prose plus Retry-After: 5 and Cache-Control: no-store:
{
"error": "Live blockchain data is not available right now — the explorer is still connecting to the Polkadex node. Please refresh in a few seconds.",
"code": "RPC_NOT_READY"
}
Branch on code === "RPC_NOT_READY" (or on the 503 status) and retry with backoff. Audit F-155: this section used to promise a short fixed error string that the server has never sent — clients matching it treated every outage as an unknown error. It is now rendered from the same constant the 503 handler uses.
Addresses
All paths that take an :address expect Polkadex-format SS58 (prefix 88, addresses start with e…). The server normalizes via toPolkadexAddress() so wallet-native prefixes (42, 0) usually also resolve, but consistency is recommended.
Quick examples
Network info (home-page summary):
curl https://explorer.polkadex.ee/api/network-info
Latest PDEX price:
curl https://explorer.polkadex.ee/api/price-latest
30-day price history (each row tagged with its data source):
curl 'https://explorer.polkadex.ee/api/price-history?days=30'
Wallet summary for a Polkadex address (replace with a real e… address):
curl https://explorer.polkadex.ee/api/wallet/esoEt6uZ9vs23yW8aqTACLf1tViGpSLZKnhPXt5Nq7vQwHGew
Search — block number, block hash, or account. Audit F-086: this line used to promise extrinsic hits too; /api/search is a live-RPC probe that does not resolve extrinsic hashes. Use /api/extrinsic-by-hash/:txHash for those:
curl https://explorer.polkadex.ee/api/search/12000000
Found a bug or a missing endpoint?
Open an issue at github.com/Polkadex-Substrate, or reach the team via the channels listed at polkadex.ee.