ETH-transactions-storage: your own Ethereum transfer index
Self-hosted Ethereum transaction indexer for native ETH and ERC-20 transfers with PostgreSQL and PostgREST
At a glance
- What is it?
- ETH-transactions-storage is a self-hosted indexer that reads blocks from your own Ethereum execution client and writes native ETH and ERC-20 transfers into PostgreSQL, then exposes them over a read-only PostgREST API. It avoids third-party data providers, rate limits and telemetry, but it only indexes direct transfer calls, so routed and internal transfers are out of scope.
- Who is it for?
- ETH-transactions-storage fits a team that needs per-address transfer history from its own node, with direct SQL access to the rows and no third-party data provider in the path. It does not fit an application that needs a complete picture of token movement, because only direct top-level transfer calls are stored and everything routed through transferFrom, multisigs, routers, batch or aggregator flows is not indexed.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 49 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem it solves: nodes cannot answer address history queries
Ethereum nodes cannot answer the question list the transactions for this address. The project says so in one sentence, and that is the whole motivation.
An execution client is built to serve the chain, not to maintain a per-account index of who paid whom. If you want the transaction history of one address, the usual answers are a third-party indexer with a rate limit and a monthly bill, or running your own indexing stack.
ETH-transactions-storage builds that index for you. It reads blocks from your execution client, writes native ETH transfers and ERC-20 transfer calls into PostgreSQL, and serves address transaction history over HTTP. The project is explicit about the properties it buys you: no third-party data provider, no rate limits, no telemetry.
The use cases are listed and they are ordinary application needs: cryptocurrency wallets rendering per-account ETH and token history, block explorers and dashboards backing address pages with SQL, accounting and treasury tools exporting transfers for reconciliation, support and compliance systems looking up on-chain activity, monitoring services watching a known address set using the optional address filter, and custom applications that want direct SQL access to transfer data.
It works with Geth, Nethermind, Besu and Erigon over HTTP, WebSocket or IPC, and with EVM-compatible networks exposing the same JSON-RPC surface. That last clause is what makes it usable beyond Ethereum mainnet, and the repository topics list the four clients explicitly.
The data flow and why the checkpoint transaction matters
The architecture is a straight line with one interesting property, and the project draws it as a diagram:
Ethereum node → ethsync.py → PostgreSQL → PostgREST → your application
JSON-RPC indexer ethtxs REST APIThe indexer polls new blocks, parses transfers, and writes each block together with its checkpoint in a single database transaction. That last clause is the design detail worth pausing on. If the block data and the checkpoint that says how far the indexer has got are written in the same transaction, then a crash can never leave the indexer believing it has processed a block whose rows are not committed, or the reverse. Restarts resume exactly where they stopped, with no manual reconciliation step and no scan for the gap.
PostgREST then turns the table into a read-only HTTP API with filtering, ordering and pagination, so there is no API code to write or maintain. That is a deliberate trade: the indexer writes SQL, and a well-known standalone API server serves the reads, rather than the project maintaining a query layer, a serialisation layer and an authorisation layer of its own.
The components map onto the files in the tree. ethsync.py is the indexer process, database.py holds the database access, and address_filter.py implements the optional address filter. The schema lives in SQL files at the root, create_tables.sql plus three index scripts: create_indexes.sql, create_indexes_add.sql and create_indexes_legacy.sql.
Running the stack with Docker Compose
The fastest path is the compose file, and the sequence is short:
git clone https://github.com/Adamant-im/ETH-transactions-storage.git
cd ETH-transactions-storage
cp .env.example .env && chmod 600 .env
cp filter/addresses.txt.example filter/addresses.txt && chmod 600 filter/addresses.txt
# set POSTGRES_PASSWORD and DOCKER_ETH_URL in .env
docker compose up -d
curl -s http://127.0.0.1:3000/max_blockTwo details in that sequence are worth copying exactly. The .env file and the address list are created with mode 600, and the reason is that the .env holds the database password and the node URL, while the address list tells the indexer which addresses to keep if the filter is on. Both are copied from checked-in examples, so you start from a template rather than an empty file.
The stack runs PostgreSQL, PostgREST, an optional local Geth dev node, and the indexer image published to ghcr.io/adamant-im/eth-transactions-storage. Nothing is built locally.
The compose file shows the shape. The database service is postgres:14 with the schema mounted into the init directory, and a healthcheck built on pg_isready. PostgREST runs on port 3000 and reads its database URI, schema and anonymous role from environment variables with defaults such as web_anon for the role and 10000 for the maximum rows. The optional publicnode service is the ethereum/client-go:stable image pinned to a release tag, because the documentation notes that the latest tag tracks unstable development builds, and it runs with the dev and period 3 flags for a three-second block time.
The curl at the end is the health check. The /max_block endpoint returns the highest processed block and the indexer version, so a non-empty answer means the indexer is running.
The API surface and the stable contract
Three endpoints are documented:
| Endpoint | Purpose | | ------------ | ------------------------------------------- | | `/ethtxs` | Indexed native ETH and ERC-20 transfers | | `/max_block` | Highest processed block and indexer version | | `/aval` | Availability probe |
The transfer query is PostgREST syntax, so the filter and the ordering are expressed in the query string. The example given asks for the last 25 native ETH transfers for an address, newest first:
# Last 25 native ETH transfers for an address, newest first
curl -s "http://127.0.0.1:3000/ethtxs?and=(contract_to.eq.,or(txfrom.eq.0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98,txto.eq.0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98))&order=time.desc&limit=25"The important property is stated plainly: endpoint names, column names and value encodings are a stable contract across upgrades. That is the difference between an indexer you can build a product on and one where a minor release renames a column.
The wider query syntax, encodings, filtering and pagination are in the REST API reference, and the compose file shows where the limits live, with PGRST_DB_MAX_ROWS defaulting to 10000 and the anonymous role set to web_anon. Since the API is read-only and served by a role with limited grants, the security model is in the database, and there is a dedicated security page in the documentation covering read-only roles and row level security.
What is indexed, and what silently is not
This is the section to read before you build anything, because the gaps are properties of the indexing logic rather than settings you can turn on.
Stored: native ETH transfers, and ERC-20 transfers submitted as a direct top-level transfer(address,uint256) call.
Not stored: internal ETH transfers, ERC-20 transfers routed through transferFrom, multisig, router, batch or aggregator flows, other token standards, and event logs. The documentation says plainly that these are properties of the current indexing logic, not settings.
That is a significant boundary for any application that cares about where funds actually went. A transfer routed through a decentralised exchange router is not indexed, because from the indexer's point of view it is a call to a contract rather than a transfer to a user. A multisig release is a call to the multisig contract. An aggregator batch is a call to the aggregator. An application that reconciles a treasury against on-chain movements will see gaps in exactly the flows a treasury uses most.
Event logs are not indexed either, so any token standard that moves value through events rather than through a transfer call is outside the index.
The storage consequence follows from the scope. The project names storage as the main planning constraint and points at three levers: a recent START_BLOCK, the recommended five-index set, and the optional address filter. In other words, you do not index the chain from genesis, you choose a starting point, and you can restrict the index to a set of addresses. The trade is explicit: a recent START_BLOCK means less history and much less data, and the address filter is what makes a large chain affordable if your use case is a known set of accounts.
Where the indexer is the wrong tool
The first limit is the scope above, and it is a hard one. If your requirement is a complete view of token movement for an address, this indexer is not enough, and no configuration changes that.
The second is the operational cost. The indexer needs an execution client of your own, a PostgreSQL instance you administer, and a PostgREST instance, and the compose file shows all of that as separate services. The Docker path is tidy, but the manual path asks you to apply the schema and grants as a PostgreSQL administrator, configure and start the indexer as the unprivileged api_user, and then create the query indexes as an administrator once the initial backfill has caught up. Index maintenance requires the table owner or an administrator, because the indexer's runtime grants do not permit it, and on a live database the documentation tells you to use CREATE INDEX CONCURRENTLY or a maintenance window.
That last point is a real operational cost for a self-hosted service. A database you administer yourself is a database you patch yourself.
The third is that the indexer is a moving part next to your node. A node that is upgraded, restarted or falls behind affects the index, and the checkpoint only guarantees that the indexer itself resumes cleanly.
The fourth is what the alternatives give you. A hosted block explorer API gives you complete coverage with a rate limit, a contract and a monthly bill, and you write no operations at all. The difference in approach is that this project keeps the data inside your infrastructure and lets you query it with SQL, at the price of maintaining the infrastructure and accepting a narrower definition of what a transfer is. If your application is a wallet showing a user their own token history, the indexer is a good fit. If it is a compliance or analytics product that needs every movement of a token, it is not.
Licence, release history and the shape of the repository
The project is released under the GPL-3.0 licence, with the text in the LICENSE file at the repository root, and the container image carries the org.opencontainers.image.licenses label of GPL-3.0-or-later. The strong copyleft terms matter for a self-hosted component like this, because if you distribute a modified version of the indexer you have obligations around the corresponding source. Read the LICENSE file yourself and take your own advice on how the terms apply to your deployment.
The release history shows a long gap and then a large jump. v2.4 came on 2024-02-09, v2.4.1 on 2024-02-13 and v2.5.0 on 2026-09-04. The repository is not archived, and the last push was on 2026-09-04, the same day as the v2.5.0 tag, so the tree is about seven weeks old relative to today. The default branch is dev rather than master, so a clone of the default branch gives you the development state.
The project is maintained by the ADAMANT developer community and cryptofoundry, and the README carries a commercial note directing people who want custom crypto software, bots, payments or blockchain infrastructure to the ADAMANT business contact page. So this is community-maintained infrastructure with a commercial backer, which is a reasonable thing to know before you depend on it.
The Python side is deliberately small. The requirements are web3 between 6.0.0 and 7.0.0, psycopg2-binary from 2.9.0, and python-dotenv between 1.0.0 and 2.0.0. The source files are ethsync.py for the indexer, database.py for the database layer, address_filter.py for the filter, and ethtest.py and pgtest.py for testing, with test_address_filter.py and test_database.py as the unit tests.
The package.json shows the tooling around the Python code, and it is a good place to look for the project's own quality bar. The lint:py script compiles every source file with py_compile, the test script runs the unit tests with unittest, lint:md runs markdownlint over the documentation, format and format:check use prettier, and the documentation is a VitePress site with dev, build and preview scripts. The node engine requirement is 22.13.0 or newer.
What to do first is narrow. Pick the START_BLOCK with the history you actually need, enable the address filter if you only care about known accounts, and read the what gets indexed page before you design the queries. Then run the stack and check /max_block to confirm the indexer is following the chain.
Editorial conclusion
ETH-transactions-storage fits a team that needs per-address transfer history from its own node, with direct SQL access to the rows and no third-party data provider in the path. It does not fit an application that needs a complete picture of token movement, because only direct top-level transfer calls are stored and everything routed through transferFrom, multisigs, routers, batch or aggregator flows is not indexed. Before you adopt it, decide the START_BLOCK carefully, since that and the address filter are the two levers that determine your storage cost, and check your node exposes the JSON-RPC surface the indexer expects, since it works with Geth, Nethermind, Besu and Erigon over HTTP, WebSocket or IPC.
Frequently asked questions
What does ETH-transactions-storage do and why would I run it?
It is a self-hosted indexer for native ETH and ERC-20 transfers. It reads blocks from your own execution client, writes transfers into PostgreSQL and serves address transaction history over a read-only PostgREST API, with no third-party data provider, no rate limits and no telemetry.
How do I start the ETH-transactions-storage stack with Docker Compose?
Clone the repository, copy .env.example to .env and filter/addresses.txt.example to filter/addresses.txt with mode 600, set POSTGRES_PASSWORD and DOCKER_ETH_URL in the .env file, then run docker compose up -d and check http://127.0.0.1:3000/max_block. The stack uses the published indexer image, so nothing is built locally.
Which Ethereum clients does the indexer work with?
It works with Geth, Nethermind, Besu and Erigon over HTTP, WebSocket or IPC, and with EVM-compatible networks exposing the same JSON-RPC surface.
What transfers does ETH-transactions-storage store, and what does it leave out?
It stores native ETH transfers and ERC-20 transfers submitted as a direct top-level transfer(address,uint256) call. It does not store internal ETH transfers, ERC-20 transfers routed through transferFrom, multisig, router, batch or aggregator flows, other token standards, or event logs, and the documentation states these are properties of the current indexing logic rather than settings.
Which API endpoints does ETH-transactions-storage expose?
Three: /ethtxs for the indexed native ETH and ERC-20 transfers, /max_block for the highest processed block and the indexer version, and /aval as an availability probe. Endpoint names, column names and value encodings are described as a stable contract across upgrades.
What licence does ETH-transactions-storage use?
It is released under the GPL-3.0 licence, with the terms in the LICENSE file at the repository root, and the container image is labelled GPL-3.0-or-later. The current release is v2.5.0, published on 2026-09-04.
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/adamant-im-eth-transactions-storage)Was this useful?