An indexer and HTTP API for AtomicAssets, AtomicMarket, AtomicTools, and related on-chain contracts on Antelope (formerly EOSIO) chains.
This codebase is a continuation of eosio-contract-api originally built by
Pink Network and is now maintained by the AtomicAssets
community. See NOTICE for the project's lineage.
AtomicAssets v2 support is released. The 2.x line indexes and serves mutable templates, schema media types, collection author succession, and the AtomicMarket royalty read layer. Moving an existing indexer, including one still on the original
eosio-contract-api? See UPGRADING.md. Short version: no Postgres 18 needed, and you can upgrade before the chain switches to the v2 contract. The 1.7 line stays available for deploys that are not ready.
-
Subscribes to a State History Plugin (SHIP) endpoint and indexes blocks into PostgreSQL in real time.
-
Exposes a REST API documented via OpenAPI / Swagger at
/docscovering: AtomicAssets (NFTs, templates, schemas, collections), AtomicMarket (sales, auctions, buy offers), AtomicTools (link claims), and curated collection lists. -
Indexes AtomicHub-specific contracts when enabled in the reader config: AtomicPacks (
atomicpacksxfor pack templates, claims, and reveal results) and AtomicDrops (atomicdropsxfor drop templates and claims). Enable per chain by adding the relevant entries toreaders.config.json:{ "handler": "atomicpacksx", "args": { "atomicpacksx_account": "atomicpacksx", "store_logs": true } } { "handler": "atomicdropsx", "args": { "atomicdropsx_account": "atomicdropsx", "store_logs": true } } -
Streams live updates via WebSockets (Socket.IO) for sales, transfers, and trades.
-
Ships a Prometheus metrics endpoint for monitoring filler health.
The service is chain-agnostic and works against any Antelope chain that has the AtomicAssets contract suite deployed. The maintainers run it in production against:
- WAX mainnet and testnet
- EOS mainnet
- Proton (XPR Network) mainnet and testnet
You will need:
- Node.js 22 (see
.nvmrc) - pnpm 10+ (
corepack enableis enough) - PostgreSQL 14+
- Redis or Valkey 7+
- A SHIP endpoint for the chain you want to index
1. Clone, install, and build.
git clone https://github.com/atomicassets/atomicassets-api.git
cd atomicassets-api
pnpm install
pnpm build # compile TypeScript to ./build (required before the start scripts)The build step is not optional: pnpm start:filler and pnpm start:server
run the compiled output in ./build, which is not committed to the repo. (The
pnpm start:* and pnpm db:* scripts will build automatically if ./build is
missing, but running pnpm build once up front makes the first run obvious.)
2. Create the database that connections.config.json points at (the schema
step below creates the tables, not the database itself):
createdb atomicassets # or: psql -c 'CREATE DATABASE atomicassets;'3. Copy the example configs and edit them for your environment. At minimum
set the Postgres credentials, the Redis/Valkey host, and the chain's RPC + SHIP
endpoints in connections.config.json.
cp config/connections.config.example.json config/connections.config.json
cp config/server.config.example.json config/server.config.json
cp config/readers.config.example.json config/readers.config.json4. Initialise the schema, then start the filler and server. The filler
writes blocks into Postgres; the server reads from Postgres and answers the API.
They are independent processes, but the server needs the schema to exist first,
so run db:schema:init before either.
When running outside the container, point CONFIG_DIR at your config/
directory; otherwise the binaries look in the image's /home/node/app/config.
export CONFIG_DIR="$PWD/config"
pnpm db:schema:init
pnpm start:filler # in one terminal: indexes blocks from SHIP into Postgres
pnpm start:server # in another: serves the REST + WebSocket APIThe API will be available on port 9000 by default with Swagger UI at http://localhost:9000/docs.
A fresh filler syncs from the chain's genesis (or the start_block in
readers.config.json), which can take a long time on mainnet. To skip the
initial sync, restore a published database dump first. See
Restore from a published dump.
A standalone container image is published on every push to main:
docker pull ghcr.io/atomicassets/atomicassets-api:mainYou can also build locally:
docker build -t atomicassets-api:local .The image entrypoint runs the API server. The filler is a separate process; run
it from the same image with command: node build/bin/filler.js.
docker-compose.yml brings up the full stack (Postgres, Valkey, the filler,
and the server) sharing your config/ directory:
cp config/connections.config.example.json config/connections.config.json
cp config/server.config.example.json config/server.config.json
cp config/readers.config.example.json config/readers.config.json
# point connections.config.json at the compose service names:
# postgres host -> "postgres", redis host -> "valkey"
docker compose up -dThe server is published on port 9000. The filler and server share the
config/ bind mount, so edit the configs on the host and restart the
services to pick up changes.
pnpm start:filler / pnpm start:server run in the foreground and stop when you
disconnect. For an always-on deployment, supervise both processes so they restart
on crash and after a reboot. Any of the following work; pick one.
docker-compose (recommended). The services above set restart: unless-stopped,
so docker compose up -d already gives you supervised, reboot-surviving filler and
server. Nothing else to configure.
PM2. A ready-made ecosystem.config.cjs is included (filler + server). Build
first, since PM2 runs node build/... directly and does not trigger the prestart*
hooks:
pnpm install && pnpm build
pnpm db:schema:init # once, before the first start
pm2 start ecosystem.config.cjs
pm2 save && pm2 startup # survive reboots
pm2 logs # follow both processesOverride CONFIG_DIR, FILLER_MAX_MEMORY, or SERVER_MAX_MEMORY by exporting
them before pm2 start.
systemd. One unit per process. Build once (pnpm build), then create
/etc/systemd/system/atomicassets-filler.service:
[Unit]
Description=atomicassets-api filler
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=atomicassets
WorkingDirectory=/opt/atomicassets-api
Environment=CONFIG_DIR=/opt/atomicassets-api/config
ExecStart=/usr/bin/node --enable-source-maps build/bin/filler.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetCopy it to atomicassets-server.service with ExecStart=… build/bin/server.js,
then:
sudo systemctl daemon-reload
sudo systemctl enable --now atomicassets-filler atomicassets-server
journalctl -u atomicassets-filler -fThree JSON files in config/ drive runtime behaviour:
connections.config.json: Postgres, Redis, RPC endpoints, SHIP endpoint.readers.config.json: which chains the filler will index, contract filters, start block, and dataset selection.server.config.json: HTTP server port, rate limits, CORS, cache policies, provider name and URL displayed in/docs.
See config/*.example.json for the full schema with comments.
Syncing a chain from genesis takes a long time. To start from a recent snapshot instead, restore a published PostgreSQL dump, then let the filler catch up the remaining blocks from SHIP.
Browse backups.atomichub.io to pick a chain
and download the latest dump, or script it against the JSON index. Each dump is a
pg_dump directory-format archive bundled into one store-only tar
(<database>.dir.tar), downloaded through the site's /download endpoint.
BASE=https://backups.atomichub.io
CHAIN=wax-mainnet # or wax-testnet, eos-mainnet, proton-mainnet, ...
# 1. Resolve the latest dump's download path, artifact name, and checksum.
read URL ART SHA < <(curl -fsSL "$BASE/api/backups.json" | python3 -c '
import sys, json
chain = "'"$CHAIN"'"
net = next(n for n in json.load(sys.stdin)["networks"] if n["network"]["chain"] == chain)
d = net["db"]["latest"]
print(d["url"], d["artifact"], d["sha256"])')
# 2. Download (a plain GET; supports resume) and verify the checksum.
curl -fSL -o "$ART" "$BASE$URL"
echo "$SHA $ART" | sha256sum -c -
# 3. Unpack the bundle (the .dir files are already zstd-compressed).
tar -xf "$ART" # -> <database>.dir/ (a pg_dump directory archive)
# 4. Create the target database (see Quickstart step 2), matching the name in
# connections.config.json.
createdb atomicassets
# 5. Restore. One job per core, and raise maintenance memory so index builds
# run in parallel and finish quickly.
PGOPTIONS='-c maintenance_work_mem=2GB -c max_parallel_maintenance_workers=4' \
pg_restore \
--dbname=atomicassets \
--no-owner --no-acl \
--jobs="$(nproc)" \
"${ART%.tar}" # the unpacked <database>.dir directoryNotes:
--jobsparallelises both the data load and the index builds. The atomicmarketseller/buyerindexes are btree (since 1.7.17), so they build in parallel; on a dump predating 1.7.17 those were hash indexes that build single-threaded and dominate restore time.maintenance_work_memis the single biggest lever for index build speed. The default (64 MB) is far too low for these tables; 1 to 2 GB is reasonable on a host with several GB of RAM.- After the restore, run
pnpm db:migrate:uponce to apply any schema migrations newer than the dump, then start the filler. It resumes from the last block in the dump and catches up to the chain head.
Error: Cannot find module '.../build/bin/filler.js'. The project has not
been compiled. Run pnpm build (which emits ./build), then retry
pnpm start:filler. ./build is intentionally not committed.
The server exits immediately on startup. The schema has not been
initialised. Run pnpm db:schema:init against the database in
connections.config.json before starting the server, and make sure the
database itself exists (createdb).
cp: config/*.example.json: No such file or directory. Run the copy
commands from the repository root; the example files live in config/.
sequence must have same owner as table it is linked to during Upgrade to 1.7.11.
The filler connects as a role that is not the owner of
atomicmarket_sales_filters_updates, and the image predates 2.2.1. Compare the
table owner with postgres.user in connections.config.json:
SELECT pg_get_userbyid(relowner) AS table_owner
FROM pg_class WHERE oid = 'atomicmarket_sales_filters_updates'::regclass;Either move to image 2.2.1 or later, or, as a superuser, create the sequence under the table owner before the next start. The migration then adopts it:
CREATE SEQUENCE atomicmarket_sales_filters_updates_seq;
ALTER SEQUENCE atomicmarket_sales_filters_updates_seq OWNER TO <table_owner>;Index creation runs for hours during a dump restore. See
Restore from a published dump. Restore with
--jobs and a raised maintenance_work_mem, and use a dump from 1.7.17 or
later (btree seller/buyer indexes).
The filler exits at COMMIT with 23503 on
atomicassets_assets_schemas_fkey, atomicassets_assets_collections_fkey, or
atomicassets_assets_templates_fkey. Every restart replays the same block
range and fails identically. The check could not find a parent row in
atomicassets_schemas, atomicassets_collections, or
atomicassets_templates. Either that row was never indexed, or it is present
and the lookup cannot see it, and those two causes need different repairs.
Settle which one first. The constraint resolves its parent through a unique
index, so an index that has lost the entry fails the check while ordinary
queries still return the row. Check the index itself, passing heapallindexed
so the check also reports heap rows the index no longer points at:
CREATE EXTENSION IF NOT EXISTS amcheck;
SELECT bt_index_check('atomicassets_schemas_pkey'::regclass, true);An error there names the damage. item order invariant violated means the
entries are stored out of sort order, so lookups binary-search past rows that
exist. A plain SELECT cannot stand in for this check, because the planner may
answer it from a sequential scan or a different index and return the row either
way.
A lost entry on character varying keys usually means collation. Their btrees
order by the collation of the key columns, and moving a data directory onto a
base image with a different glibc or ICU invalidates that order silently.
PostgreSQL 15 and later record the version the database was built with:
SELECT datcollversion,
pg_database_collation_actual_version(oid) AS actual_version
FROM pg_database WHERE datname = current_database();Differing versions mean every text index in the database is suspect, not only
this one. PostgreSQL 14 does not record this, so on 14 treat a base-image
change as reason enough to reindex. Repair with
REINDEX DATABASE CONCURRENTLY <database>, then, on 15 and later,
ALTER DATABASE <database> REFRESH COLLATION VERSION. Refreshing first clears
the warning without repairing anything.
Reindexing one index unwedges the filler, but stop only there if the collation
is unchanged. Rebuilding concurrently needs room for a second copy of each
index and leaves invalid _ccnew indexes to drop if a run fails. System
catalogs are separate: they need REINDEX SYSTEM <database>, which cannot run
concurrently. Read a failure of the form
could not create unique index ... Key ... is duplicated as data damage rather
than a reindex problem: the broken ordering also let the uniqueness check pass
rows it should have rejected, and those duplicates have to go before the index
can be rebuilt.
When the index is sound, the parent row really is missing. The rest of this entry covers that case.
These constraints are added NOT VALID, so the scan that would report existing
violations never runs. Later statements that write those rows are still checked,
and because the constraints are DEFERRABLE INITIALLY DEFERRED, that check
lands at COMMIT and fails the whole block group. An orphaned asset therefore
stays silent until a later block writes it, which is often a sale or a transfer
of an asset minted long before.
Compare the parent tables against a complete database for the same chain, such as a public API for that chain. The numbers below mean nothing in isolation, so run each query on both databases:
SELECT count(*) FROM atomicassets_schemas;
SELECT (created_at_block / 10000000) * 10000000 AS block_bucket, count(*)
FROM atomicassets_schemas GROUP BY 1 ORDER BY 1;Equal counts do not prove the parent row is present, since one absence can hide
behind an unrelated extra row, but a bucket that falls short of the complete
database shows which stretch of history this database never received. Full
buckets missing only a handful of individual rows mean it was seeded from an
incomplete source. How far the filler has ingested is a separate question,
answered by SELECT name, block_num FROM contract_readers;, not by the newest
row in a parent table. Scope any orphan scan to one collection, because
atomicassets_assets holds hundreds of millions of rows on a busy chain.
Copying the missing parent rows from a complete database clears the current
block, but the next absent row wedges the filler the same way. Restoring a
published dump is the durable fix. See
Restore from a published dump.
pnpm start:reconcile seeds v2 contract state and template deletions and does
not rebuild base rows.
pnpm build # compile TypeScript to ./build
pnpm check-types # type-only check (no emit)
pnpm test # run the unit test suite (mocha)
pnpm lint # ESLint
pnpm dev:server # rebuild + run server with --trace-warnings
pnpm dev:filler # rebuild + run filler with --trace-warningsIntegration tests require a running Postgres and the connection config:
pnpm test:e2e:ciThis project uses semantic versioning. Tagged releases are published to GitHub Releases and the corresponding container image tags are pushed to GHCR. RELEASING.md covers how a release is cut and what its notes carry, and CHANGELOG.md is where the notes of each release are written.
Two lines are published in parallel, so pick the tag that matches how much you
want moving under you. Everything below lives at
ghcr.io/atomicassets/atomicassets-api.
| Tag | Line | Moves? | What it tracks |
|---|---|---|---|
2.0.0 |
2.x | Never | One exact release. Pin an exact tag in production. |
2.0 |
2.x | Yes | The newest patch in the 2.0 minor. |
latest |
2.x | Yes | The newest stable release on the current major. Convenient for a first look, wrong for a production pin. |
1.7.25 |
1.7 | Never | One exact release on the maintenance line. |
1.7 |
1.7 | Yes | The newest patch on the maintenance line, for deploys not yet ready for v2. |
The two lines differ in how their git tags are written: 2.x releases are tagged
2.0.0 and 1.7 releases v1.7.25. The image tag drops the v either way, so
git v1.7.25 publishes image 1.7.25. Release candidates are published as
2.0.0-rcN; being prereleases they never move 2.0 or latest, so you have to
ask for one by name.
The codebase carries the full release history from the upstream
pinknetworkx/eosio-contract-api project (v1.0.0-rc1 through v1.3.21)
plus all subsequent work done while it lived inside the atomichub monorepo.
Issues and pull requests are welcome. See CONTRIBUTING.md for development setup, commit conventions, and the PR review process. Security reports go through SECURITY.md.
- Pink Network and Spielworks Markets GmbH built the
original
eosio-contract-apiand the AtomicAssets / AtomicMarket / AtomicTools contract suites that this service indexes. - The AtomicHub team and FACINGS for carrying the codebase forward and running it at scale across multiple Antelope chains.
- Everyone who has filed issues, opened PRs, and run nodes against this indexer.