# Etherscan API compatibility

ApeScan answers every action in Etherscan's API V2 reference (docs.etherscan.io, checked 2026-10-19).
To move a client from Etherscan, change its base URL to the ApeScan API. Keep the rest of the request.

| Item | ApeScan behaviour |
|---|---|
| Endpoints | `GET` and `POST` on `/api` and `/v2/api`. `POST` takes a form or JSON body; action and filter body fields win over query fields. Conflicting credentials return HTTP 401. |
| `chainid` | Optional. One deployment serves one chain (ApeChain `33139`, Curtis `33111`). Any other value gets Etherscan's "Missing or unsupported chainid parameter …" error. |
| `apikey` | Optional for ordinary point lookups; required for the bulk and historical actions below. Supply one valid key in query/body `apikey`, an `apikey` header, or an `x-api-key` header. Invalid or conflicting credentials return HTTP 401. |
| Envelope | `{status, message, result}` with string values, as Etherscan. `module=proxy` answers JSON-RPC `{jsonrpc, id, result \| error}`. |
| Empty lists | `status "0"`, `result []`, and Etherscan's message: `No transactions found`, `No records found` (logs), `No tokens found`, `No token holders found` or `No data found`. |
| Paging | `page` × `offset` ≤ 10,000, as Etherscan. `getLogs` returns at most 1,000 rows a page. |
| Rate limit | HTTP 429 with `Retry-After`. Per-second limits retain the "Max rate limit reached" prefix; work-budget errors identify the exhausted usage allowance. Clients must honor the advertised delay. |
| Unknown action | "Error! Missing Or invalid Action name (module.action)". |
| `/v2/chainlist` | Lists the one chain this deployment serves. |

Status in the tables:

- **Supported**: same parameters, fields and types as Etherscan.
- **Partial**: answered in Etherscan's shape; the note names the fields or range that differ.
- **Not applicable**: the figure does not exist on an Arbitrum Orbit chain. The answer is the one Etherscan gives when there is no data.

"Indexed" means ApeScan's ClickHouse index. Its history starts at the index start block, and a backfill
extends it toward genesis. Answers read from the node (balances, supply, calls) cover every block.

## API keys and public access

Ordinary point lookups remain public: contract source and ABI, transaction status and receipts,
current balances and supply, block headers, and current-state proxy calls. Public explorer pages
and native page-two navigation remain available without an API key.

The following Etherscan reads require a valid API key on both `/api` and `/v2/api`:

| Module | Actions or input |
|---|---|
| `account` | `txlist`, `txlistinternal`, `tokentx`, `tokennfttx`, `token1155tx`, `balancehistory`, `tokenbalancehistory`, `addresstokenbalance`, `addresstokennftbalance`, `addresstokennftinventory`, `fundedby`, `getdeposittxs`, `getwithdrawaltxs` |
| `logs` | `getLogs`, with any address, topic, or block filter |
| `token` | `tokenholderlist`, `topholders`, for every page and page size |
| `stats` | `tokensupplyhistory`, `dailytx`, `dailyblkcount`, `dailyavgblocksize`, `dailyavgblocktime`, `dailyavggaslimit`, `dailygasused`, `dailynetutilization`, `dailytxnfee`, `dailyavggasprice`, `dailynewaddress`, `dailyblockrewards`, `dailyuncleblkcount`, `ethdailyprice`, `ethdailymarketcap` |
| `nametag` | `exportaddresstags` |
| `account` | `balance` or `balancemulti` with an explicit historical `tag` |
| `proxy` | `eth_call`, `eth_getCode`, `eth_getStorageAt`, or `eth_getTransactionCount` with an explicit historical `tag` |
| `proxy` | `eth_getBlockByNumber` with `boolean=true`, which exports full transactions |

Current tags are `latest`, `pending`, `safe`, and `finalized`. An omitted tag uses the handler's
current-state default. Other explicit tags, including `earliest` and numeric block tags, require a
key on the archive actions above. Block-header and transaction-hash point lookups remain public.
The MCP `get_logs` tool has the same key requirement; MCP metadata and ordinary point tools remain public.
MCP batches are not supported; send one JSON-RPC message per HTTP request. An array body returns
HTTP 400 before any tool runs.

GET query inputs and POST form or JSON inputs follow the same policy. On POST, action and filter
body fields win over query fields, including `module` and `action`. Conflicting nonempty credentials return HTTP 401
across query, body, and accepted key headers; body precedence does not select a different key.
A query claiming a public action cannot bypass a historical action in the body. Native full CSV
exports also require a key through the web proxy; public page CSV exports remain limited to 100 rows.
`Authorization: Bearer` remains the SIWE session mechanism.

For example, `GET /api?module=logs&action=getLogs&fromBlock=1&toBlock=100` without a key returns
HTTP 401 with this Etherscan-compatible response:

```json
{"status":"0","message":"NOTOK","result":"API key required for bulk exports, deep pagination, and historical API requests. Create a free key at /api-docs."}
```

Add `apikey=<valid-key>` to run that request. A public contract lookup such as
`GET /api?module=contract&action=getsourcecode&address=<address>` needs no key. API access policy
does not silently truncate results or alter balances, held/supply totals, ordering, pagination,
existing query limits, or cache keys. Missing keys and invalid keys return HTTP 401; exhausted
[fixed-window work budgets](operations.md#usage-work-budgets) return HTTP 429 with `Retry-After`.
Shared admission failure returns HTTP 503 for keyed or expensive work.

## account

| Action | Status | Notes |
|---|---|---|
| `balance` | Supported | Node read. `tag` is a block tag. |
| `balancemulti` | Supported | Up to 20 addresses. |
| `balancehistory` | Supported | Node archive read at `blockno`. |
| `txlist` | Supported | Indexed. Also the advanced filter: `from`, `to`, `fromto_opr` (and \| or). |
| `txlistinternal` (address) | Supported | Indexed value transfers and creations, the same rows as Etherscan's feed. Advanced filter as `txlist`. |
| `txlistinternal` (txhash) | Supported | Every indexed call frame of the transaction. |
| `txlistinternal` (block range) | Partial | `startblock` and `endblock` without an address. The span is at most 10,000 blocks. |
| `tokentx`, `tokennfttx`, `token1155tx` | Supported | Indexed. By `address`, `contractaddress`, or both; advanced filter as `txlist`. |
| `tokenbalance` | Supported | Node read; `tag` must be `latest`. |
| `tokenbalancehistory` | Supported | Node archive read at `blockno`. |
| `addresstokenbalance` | Supported | Indexed ERC-20 holdings. `TokenPriceUSD` is present when market data is configured. |
| `addresstokennftbalance` | Supported | Indexed ERC-721 holdings, items per collection. |
| `addresstokennftinventory` | Partial | Indexed ERC-721 token IDs in numeric order, 1,000 a page. ERC-2309 batch-minted items show only after a transfer event names them. |
| `fundedby` | Partial | First incoming native transfer (transaction or internal call) within the indexed range. |
| `getdeposittxs` | Partial | Arbitrum deposit (`0x64`) and retryable (`0x69`) transactions received. `L1transactionhash`, `L1TxOrigin`, `queueIndex` and the token fields other than `tokenSentTo` and `tokenValue` are empty. |
| `getwithdrawaltxs` | Partial | ArbSys `L2ToL1Tx` withdrawals started by the address. `tokenValue` is the native value; `status` and `L1transactionhash` are empty. Relay status: `GET /v1/transactions/{hash}/l2-to-l1`. |
| `getminedblocks` | Not applicable | Nitro has no block producers paid per block: `No transactions found`. |
| `txsBeaconWithdrawal` | Not applicable | No beacon chain: `No transactions found`. |
| `txnbridge` | Not applicable | No Plasma bridge: `No transactions found`. |

## contract

| Action | Status | Notes |
|---|---|---|
| `getabi`, `getsourcecode` | Supported | Verified contracts on this deployment; proxy implementations are detected on chain. Sources verified on Etherscan are archived and recompiled here by `import-etherscan` ([operations](operations.md#contract-verification)). |
| `getcontractcreation` | Partial | Up to 5 indexed contracts. `contractCreator` is the deploying account; `contractFactory` names the creating contract. `creationBytecode` is filled for direct deployments and empty for factory deployments. |
| `verifysourcecode` | Supported | `codeformat` `solidity-single-file`, `solidity-standard-json-input` and `vyper-json`. Stylus and zkSync formats are refused with a message. |
| `checkverifystatus` | Supported | |
| `verifyproxycontract`, `checkproxyverification` | Supported | Confirms the proxy detected on chain (EIP-1967, beacon, EIP-1822, EIP-1167). |

## transaction

| Action | Status | Notes |
|---|---|---|
| `getstatus`, `gettxreceiptstatus` | Supported | Node read. |

## block

| Action | Status | Notes |
|---|---|---|
| `getblockreward` | Supported | `blockReward` and `uncleInclusionReward` are `0`; Nitro pays no block reward. |
| `getblockcountdown` | Supported | Estimate from the last 500 indexed blocks. |
| `getblocknobytime` | Supported | `closest` before or after. |
| `getblocktxnscount` | Supported | Indexed block. Token counts are transfer events: `Transfer` with three topics (ERC-20) or four (ERC-721), `TransferSingle` and `TransferBatch` (ERC-1155). |

## logs

| Action | Status | Notes |
|---|---|---|
| `getLogs` | Partial | By address, topics, or both, with `topicX_Y_opr` (and \| or) between consecutive supplied topics. Every request without `address`, including a `topic0` filter, covers at most 100,000 blocks inclusive. Wider ranges return an explicit `status "0"`, `message "NOTOK"` error; results are never clipped. |

For a topic-only scan, `fromBlock=0&toBlock=99999` is allowed; `fromBlock=0&toBlock=100000`
is refused. Continue with `fromBlock=100000&toBlock=199999`, or supply an address to query a wider
range. Each accepted range retains ascending block/log ordering, the 1,000-row page cap, and `page`
pagination. Split an inclusive range into non-overlapping windows, then paginate each window before
advancing. `latest` resolves to the indexed head before the same block-count check.

## proxy

| Action | Status | Notes |
|---|---|---|
| `eth_blockNumber`, `eth_getBlockByNumber`, `eth_getBlockTransactionCountByNumber`, `eth_getTransactionByHash`, `eth_getTransactionByBlockNumberAndIndex`, `eth_getTransactionCount`, `eth_getTransactionReceipt`, `eth_call`, `eth_getCode`, `eth_getStorageAt`, `eth_gasPrice`, `eth_estimateGas` | Supported | Passed to the node after strict parameter checks. Reverts keep their data; other node errors are not shown. |
| `eth_getUncleByBlockNumberAndIndex` | Not applicable | `result: null`, the node's answer: Nitro blocks have no uncles. |
| `eth_sendRawTransaction` | Supported | See [Broadcasts](#broadcasts). |

### Broadcasts

`eth_sendRawTransaction` is the only write. Send the signed bytes as `hex`, by `GET` or (for long
transactions) `POST`.

1. ApeScan reviews the bytes first: one legacy, EIP-2930 or EIP-1559 transaction, signed for the served
   chain, at most `SIGNED_TRANSACTION_BYTES`. Anything else gets `-32602 Invalid params`, and the node is not called.
2. The node receives each reviewed transaction once. The outcome is remembered per hash, so a retry
   returns the first outcome and never sends twice. This is the same runtime as `POST /v1/transactions/broadcast`.
3. The answer is `result: <hash>` when the node accepts it, or the node's own JSON-RPC error.

Limits: each client counts in the small rate bucket (a fifth of its tier). `POST /api` also counts
against the anonymous hourly cap for verification submissions (`RATE_LIMIT_VERIFY_HOURLY`, default 30).
The process admits at most 2 submissions a second and 2 at a time (`BROADCAST_LIMITS`).

## stats

| Action | Status | Notes |
|---|---|---|
| `ethprice` | Partial | APE in USD with its time. There is no BTC pair: `ethbtc` and `ethbtc_timestamp` are empty. |
| `ethsupply`, `ethsupply2` | Not tracked | Error text: native APE is bridged in and out, and the index holds no supply ledger from genesis. |
| `tokensupply` | Supported | Node read of `totalSupply()`. |
| `tokensupplyhistory` | Supported | Node archive read of `totalSupply()` at `blockno`. |
| `dailytx`, `dailyblkcount`, `dailyavgblocksize`, `dailyavgblocktime`, `dailyavggaslimit`, `dailygasused`, `dailynetutilization`, `dailytxnfee` | Supported | Indexed UTC days from `startdate` to `enddate` (inclusive), at most 365 days a request, `sort` asc \| desc. Fees are in APE. |
| `dailyavggasprice` | Partial | `avgGasPrice_Wei` only; `maxGasPrice_Wei` and `minGasPrice_Wei` are empty. |
| `dailynewaddress` | Partial | Only while the index holds every block from genesis; otherwise an error says so. |
| `dailyblockrewards`, `dailyuncleblkcount` | Not applicable | One row per indexed day with zero rewards and uncles. |
| `ethdailyprice`, `ethdailymarketcap` | Partial | Daily APE snapshots recorded by this deployment; `supply` is empty. Without the price store, an error says so. |
| `dailyavghashrate`, `dailyavgnetdifficulty` | Not applicable | No proof of work: `No data found`. |
| `dailyensregister` | Not applicable | ENS registrations are not indexed: `No data found`. |
| `chainsize`, `nodecounthistory` | Not applicable | Node disk size and peer counts are not tracked: `No data found`. |
| `nodecount` | Not tracked | Error text. |

## token

| Action | Status | Notes |
|---|---|---|
| `tokeninfo` | Partial | Indexed name, symbol, decimals, type and supply. Project links, `blueCheckmark` and `tokenPriceUSD` are empty. |
| `tokenholderlist` | Supported | Exact indexed aggregate balances in raw units, largest first with address as the tie-breaker. All pages reuse one ranked result within the existing 10,000-row result window, cached for at most five minutes. Holder counts and reported token supply never substitute for balances. |
| `tokenholdercount` | Supported | Indexed holders with a positive balance. |
| `topholders` | Partial | Up to 1,000 holders in token units. `TokenHolderAddressType` is `C` for contracts the index created, `E` otherwise. |

`tokenholderlist` keeps the same `page` and `offset` inputs and exact decimal quantities. For example,
`page=2&offset=100` returns ranks 101–200 from the same ordered result as page 1. A page requested near
cache expiry does not extend that result's age. Requests beyond the 10,000-row result window return
an explicit error. Every page size slices one exact prefix; pages do not store duplicate payloads.
Each chain admits at most 32 token/snapshot identities, with a 4 MiB limit per prefix. Unused admission
members expire within seven minutes; the membership set itself expires within eight minutes.

After the snapshot lookup, a cold holder prefix waits at most two seconds before returning
HTTP 200 with `status: "0"`, `message: "NOTOK"`, and
`result: "Error! Token holder list is warming; retry shortly"`. It includes `Retry-After: 2` and
`Cache-Control: no-store`. Retry the same query after that delay. The shared ERC-20 balance scan has
a 50-second cancellation budget; requests do not start duplicate token aggregations while it runs.
Busy results start refreshing during their final minute, and results older than five minutes are
never served. Full admission, unavailable shared cache, oversized results, and truncated or
incompletely repaired ERC-2309 reconstructions return the same warming response without storing a
successful result. The initial snapshot lookup keeps its existing request query budget.

A disconnected HTTP caller stops waiting without canceling other readers. The independently owned
computation signals cancellation after 50 seconds and drains its children before releasing its
72-second fenced lease. Range cache children use independent 12-second signals; the final range
aggregate uses the owner's signal. Cancellation is cooperative. A late owner cannot overwrite a
successor's result or delete its lease. Cold ranking still scans token history; caching shares that
cost across pages. An upstream failure still returns the existing retryable Etherscan error.

## gastracker

| Action | Status | Notes |
|---|---|---|
| `gasoracle` | Supported | Gwei. Nitro ignores priority fees, so the safe, proposed and fast prices are all `eth_gasPrice`. `gasUsedRatio` is the last five blocks. |
| `gasestimate` | Supported | Seconds: the next block when `gasprice` meets the current price; an error below it. |

## nametag, ens, getapilimit

| Action | Status | Notes |
|---|---|---|
| `nametag.getaddresstag` | Partial | Up to 100 addresses, from ApeScan's public label registry. `reputation` is `0`; an untagged address has empty fields. |
| `nametag.getlabelmasterlist`, `nametag.exportaddresstags` | Partial | Answered inline in the legacy shape, not as a download link. `label=all` exports every tag. |
| `ens.forwardresolve` | Supported | The ApeChain (ENSIP-11) record first, then the Ethereum record. |
| `getapilimit.getapilimit` | Partial | ApeScan limits calls per second, not daily credits: `limitInterval` is `second`. |
