etherscan-api: a promise-based client for the Etherscan V2 multichain API
A node.js wrapper for the etherscan api to fetch data and metadata from the ethereum blockchain
At a glance
- What is it?
- etherscan-api is a TypeScript client for Etherscan's V2 API that takes one API key across every supported chain, uses no runtime dependencies, and ships a custom transport hook so you can control proxies, retries and logging. It suits a Node service that needs blockchain data without a full web3 stack, and the API key in the URL is a real operational concern the documentation takes seriously.
- Who is it for?
- etherscan-api fits a Node.js service that needs account balances, contract data or transaction metadata from Etherscan and would rather not pull in a web3 library, since the client is a thin promise wrapper over one base URL with a chainid parameter and no runtime dependencies.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 25 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One key, many chains, one base URL
etherscan-api is a wrapper for the etherscan.io API that returns promises, written in TypeScript and shipped as an ES module with bundled type declarations, requiring Node.js 22 or newer.
The basic usage is a two-liner. You initialise the client with your key and call a method:
import { init } from 'etherscan-api';
const api = init('YourApiKey');
const balance = await api.account.balance('0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae');
console.log(balance);The method organisation is namespaced, account.balance being one of them, which keeps a large API surface readable.
The multichain design is the substantive change. Etherscan deprecated the V1 API on 2025-08-15, and this library now talks to a single base URL, https://api.etherscan.io/v2/api, selecting the network with a chainid query parameter. One API key works across all chains, which removes the old pattern of holding a separate key per network.
The chain is the second argument to init, either a name or a numeric chainid:
import { init } from 'etherscan-api';
// apikey, chain, timeout
const api = init('YourApiKey', 'sepolia', 3000);The named chains are mainnet, homestead and ethereum for chainid 1, sepolia for 11155111, hoodi for 560048, arbitrum for 42161, optimism for 10, base for 8453, polygon for 137, bsc for 56, avalanche for 43114 and avalanche_fuji for 43113. Any other chain is reachable by passing its numeric chainid directly, with the readme giving Linea's 59144 as the example.
The retired testnets are worth a mention because the failure mode is deliberate. ropsten, rinkeby, kovan, goerli, holesky, morden and arbitrum_rinkeby have been removed and now throw an error with a helpful message pointing at sepolia or hoodi. A library that quietly mapped an old chain name to something wrong would be worse than one that refuses.
No runtime dependencies, and a transport you can replace
The library has no runtime dependencies. Requests go through Node's built-in https module rather than through axios or fetch, and the readme explains why that is exposed: if you need custom networking, a proxy, retries or a different agent, you pass your own transport as the fourth argument to init.
The transport contract is small. It receives the fully-qualified URL and must resolve with the parsed JSON body, and it is handed a second argument carrying the timeout, the method and the body. The method and body are only set for the POST contract-verification endpoints, and for read-only use you can ignore them.
import { init } from 'etherscan-api';
// (url, { timeout, method, body }) => Promise<object>
// `method`/`body` are only set for the POST contract-verification endpoints;
// for read-only use you can ignore them.
async function request(url, { timeout, method = 'GET', body }) {
const res = await fetch(url, {
method,
body,
headers: body ? { 'Content-Type': 'application/x-www-form-urlencoded' } : undefined,
signal: AbortSignal.timeout(timeout),
});
// fetch resolves on any HTTP status; surface non-2xx as an error.
if (!res.ok) throw new Error(`Request failed with status code ${res.status}`);
return res.json();
}
const api = init('apikey', null, 10000, request);Two details in that example are worth copying if you write your own transport. AbortSignal.timeout turns the timeout into a real cancellation rather than a promise that never settles, and the non-2xx check is explicit, because fetch resolves on any HTTP status and a transport that forgets this turns a 429 rate limit into a successful parse of an error body.
The init function also takes an options object, where maxResponseBytes and allowInsecure are plain fields, and both are passed to the transport on every request, including a custom one:
import { init } from 'etherscan-api';
const api = init({
apiKey: 'apikey',
chain: 'mainnet',
timeout: 10000,
maxResponseBytes: 200 * 1024 * 1024,
});So the transport seam is not an escape hatch for exotic cases. It is where you put the retry logic that any production caller of a rate-limited public API ends up writing.
The API key is in the URL, and the readme is blunt about it
The security notes are the most useful part of the readme, and they start with a problem that cannot be designed away.
The API key travels in the request URL on GET requests. Etherscan requires apikey as a query parameter, so it is part of every GET request URL. The advice is to treat full request URLs as secrets: do not log them, and be careful with proxies, APM tools and access logs that capture URLs. A custom transport receives the URL containing the key, so it must never write it to logs verbatim. The library itself is careful here, never putting the URL or the key into thrown errors.
The exception is the POST contract-verification endpoints, where the key is sent in the form body and the URL is a bare /v2/api. So the exposure is specific to reads, and it is broad, because reads are most of what you do.
The second note is about the default transport refusing cleartext. Requests go to https://api.etherscan.io over TLS with certificate validation on, and if a request somehow targets an http:// URL the default transport rejects it rather than sending the key unencrypted. The way to bypass that is to pass allowInsecure: true in the transport options, and the readme says to do so only if you deliberately need cleartext, for example for a local test server.
The third note is a cap on the response body at 50 MB, described as a guard against a memory-exhaustion response, overridable with maxResponseBytes. A cap on a response you did not ask to be small is a sensible default for a library that fetches data whose size you do not control, and the override exists for the case where you genuinely expect a large payload.
Taken together these three notes describe a library that is careful about what it will do with your key by default and tells you exactly where the sharp edges are. That is better than most client libraries manage.
Development workflow and how releases happen
The repository is small and the scripts in package.json describe the whole workflow.
Source lives in ./src as TypeScript and compiles to ./lib as ES modules with .d.ts declarations. The build script runs tsc, the typecheck script runs tsc with noEmit and replaces the old linter, and the docs script generates API documentation with TypeDoc. There is a typedoc.json at the root configuring that.
The test script is npm test, which is node --test, and the pretest hook runs the build first, so the tests always run against freshly compiled output. The readme describes it as a fully mocked test suite that needs no API key, which is the property that matters for a client like this: the tests do not depend on Etherscan being up, not being rate limited, or not costing you quota.
The versioning hooks are deliberate. preversion runs npm test, and postversion pushes with follow-tags. The release notes are generated by the release workflow when a v* tag is pushed, and the workflow lists the commits since the previous tag as the body of the GitHub release. So changelogs are derived from commits rather than written by hand, which keeps them accurate at the cost of requiring clean commit messages.
The published package contains only lib, with main pointing at ./lib/index.js and types at ./lib/index.d.ts. The exports map exposes the root entry with types and import conditions plus ./package.json, which means the package is ESM-only, and the readme's tutorial page explicitly covers use from CommonJS. Node's require of an ESM package still works in recent versions, and that tutorial covers the details.
The dev dependencies are three: the Node types, typedoc and TypeScript itself. No test framework, because node --test is built in. The MIT licence is in LICENSE.md, and the author is Sebastian Schürmann.
Where etherscan-api is the wrong choice
The first limitation is that you are dependent on a third-party service with its own rate limits, its own key management and its own uptime record. Etherscan's API is a hosted endpoint with a free tier that is rate limited, and this library cannot change that. A product that needs guaranteed access to on-chain data has a different requirement, and the answer is usually to run your own indexer or node rather than to wrap someone else's API more carefully.
The second is the key in the URL, and it is not fixable from the client side. Every GET request URL contains your key, which means any component in the request path that logs URLs can capture it: an HTTP proxy, an APM agent, a load balancer's access log, an error tracker that records the request URL. The readme warns about all of these, but a warning does not prevent the leak, it only tells you where to look. A design where the key travels in a header would simply not have this problem.
The third is that this is an API wrapper, not a blockchain library. It fetches data that Etherscan has already indexed. It cannot read a block you make right now, it has no notion of a pending transaction, it cannot sign anything, and it cannot deploy a contract. If you need to send a transaction or read chain state directly, you need a web3 library, and this library will not do that job.
The fourth is the Node 22 floor. The manifest requires node >=22, and the readme states Node.js 22 or newer is required. That is a current floor, which is fine for a new project, but it means the package cannot be used in an older runtime without downgrading to a pre-13 release, and 13.0.0 was itself a major version with the V2 API migration in it.
The fifth is the breadth of supported chains against the reality of the API. The named chain list covers the common networks, and any chainid can be passed numerically, which is a good escape hatch. But a chainid the library has never heard of is not a chain the Etherscan V2 API necessarily serves, so the numeric fallback can produce a valid request that returns nothing useful, and the error you get will be about the API rather than about your chain choice.
Editorial conclusion
etherscan-api fits a Node.js service that needs account balances, contract data or transaction metadata from Etherscan and would rather not pull in a web3 library, since the client is a thin promise wrapper over one base URL with a chainid parameter and no runtime dependencies. It is the wrong choice if you need the key kept out of URLs entirely, because Etherscan requires the apikey as a query parameter on GET requests, so the readme tells you to treat full request URLs as secrets and never log them. Before adopting it, read the security notes about the key travelling in the URL, decide whether the default 50 MB response cap and the TLS-only default suit your setup, and note that the last push was on 2026-09-27 with v13.0.0 released the same day under an MIT licence.
Frequently asked questions
How do I install and use etherscan-api?
Run npm install etherscan-api --save, then initialise the client with init('YourApiKey') and call a method, for example await api.account.balance('0xde0b...'). The library is written in TypeScript, shipped as an ES module with bundled type declarations, and requires Node.js 22 or newer.
How do I select a chain in etherscan-api?
Pass a chain name or numeric chainid as the second argument to init, for example init('YourApiKey', 'sepolia', 3000). Named chains include mainnet, sepolia, hoodi, arbitrum, optimism, base, polygon, bsc, avalanche and avalanche_fuji, and any other chainid can be passed numerically, such as 59144 for Linea.
Does etherscan-api use the same API key for every chain?
Yes. Since Etherscan deprecated the V1 API on 2025-08-15, the library talks to a single base URL, https://api.etherscan.io/v2/api, and selects the network with a chainid query parameter, so one API key works across all chains.
How does etherscan-api handle the API key in requests?
The key travels in the URL on GET requests, because Etherscan requires apikey as a query parameter, so the readme says to treat full request URLs as secrets, never log them, and be careful with proxies, APM tools and access logs. The POST contract-verification endpoints are the exception, since there the key is sent in the form body.
Can I use a proxy or custom HTTP handling with etherscan-api?
Yes. The library has no runtime dependencies and requests use Node's built-in https module, and you can pass your own transport as the fourth argument to init. It receives the fully-qualified URL and must resolve with the parsed JSON body, and it is given the timeout, method and body options.
What licence is etherscan-api released under?
MIT, with the licence in LICENSE.md. The last push to the repository was on 2026-09-27 and the newest release is v13.0.0 from the same day, following v12.2.0 and v12.1.0 earlier that month.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sebs-etherscan-api)Was this useful?