Polkadex Mainnet Explorer

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:

BehaviorWhat 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:

CallerWhy 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

TierEndpointsCache-Control
ShortHigh-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
MediumSlower-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
LongHistorical 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 cachePer-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 blocks
  • GET /api/block/:number — single block with extrinsics + events
  • GET /api/events — most recent on-chain events
  • GET /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 movement
  • GET /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 forever
  • GET /api/extrinsic/:block/:txHash — single-extrinsic detail
  • GET /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 + commission
  • GET /api/validator/:address — per-validator era history
  • GET /api/holders — top-balance accounts
  • GET /api/account/:address — account-level summary
  • GET /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 those
  • GET /api/staking-rewards/:address — per-address reward history
  • GET /api/staking-rewards-status — reward-backfill progress
  • GET /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, errors
  • GET /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= supported
  • GET /api/runtime — runtime spec/impl version; ?at= supported
  • GET /api/decode/:block — every extrinsic decoded argument by argument (name, type, human, JSON, raw hex). Filters ?section= ?method= ?index=; matching ignores case and underscores
  • POST /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 address
  • GET /api/proxy-types — the runtime's ProxyType enum variants, read out of chain metadata rather than hardcoded
  • GET /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 cacheable
  • POST /api/labels/:address — add or replace your own label for that address
  • DELETE /api/labels/:address — remove your own label
  • POST /api/labels/:address/:signer/vote — up/down-vote someone else's label
  • POST /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 live
  • GET /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-up
  • GET /api/treasury — treasury balance + proposals (open + historical)
  • GET /api/democracy — referenda + public proposals
  • GET /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 target
  • GET /api/email/preferences?token=<t> — fetch current event preferences
  • POST /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 items
  • GET /api/discussions/:id — single thread with posts

Authenticated

  • POST /api/auth/challenge — request a sign-in nonce
  • POST /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 ms
  • POST /api/auth/logout — invalidate the session token
  • POST /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 published
  • GET /version.json — the frontend's equivalent, served as a static file
  • GET /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.