Skip to content

Latest commit

 

History

2,959 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

atomicassets-api

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.

What it does

  • 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 /docs covering: 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 (atomicpacksx for pack templates, claims, and reveal results) and AtomicDrops (atomicdropsx for drop templates and claims). Enable per chain by adding the relevant entries to readers.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.

Supported chains

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

Quickstart

You will need:

  • Node.js 22 (see .nvmrc)
  • pnpm 10+ (corepack enable is 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.json

4. 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 API

The 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.

Docker

A standalone container image is published on every push to main:

docker pull ghcr.io/atomicassets/atomicassets-api:main

You 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

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 -d

The 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.

Keeping it running in production

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 processes

Override 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.target

Copy 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 -f

Configuration

Three 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.

Restore from a published dump

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 directory

Notes:

  • --jobs parallelises both the data load and the index builds. The atomicmarket seller / buyer indexes 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_mem is 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:up once 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.

Troubleshooting

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, since heapallindexed is what reports a heap row 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. 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.

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.

Development

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-warnings

Integration tests require a running Postgres and the connection config:

pnpm test:e2e:ci

Releases

This 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.

Contributing

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.

License

MIT. See LICENSE and NOTICE.

Acknowledgments

  • Pink Network and Spielworks Markets GmbH built the original eosio-contract-api and 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.

About

The official AtomicAssets API service.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages