yarn install
yarn db:migrate up
yarn lint:check
yarn test
With defaults:
yarn start
Starting at an arbitrary block (only works immediately after initial DB migration):
START_HEIGHT=800000 yarn start
This repo ships skills under .claude/skills/ that Claude Code
(claude.ai/code) auto-loads when invoked from the
repo root:
ar-io-gateway-operator— operator runbook covering the ANS-104 pipeline, ClickHouse, ArNS resolution, the observer/cranker, and common pitfalls. Includesscripts/health-checkfor a one-screen gateway health snapshot.release— version bumps, image SHA pinning, tag and GitHub-release creation.testing— picks the right test layer (unit / property / e2e / auto-verify / parquet integration / load) for a given change.
The skills are plain Markdown — readable as runbooks even if you don't use Claude Code.
docs/INDEX.md is the index of the documents in docs/.
You can run the ar.io gateway as a standalone docker container:
docker build . -t ar-io-core:latest
docker run -p 4000:4000 -v ar-io-data:/app/data ar-io-core:latestTo run with a specified start height (sets height on first run only):
docker run -e START_HEIGHT=800000 -v $PWD/data/:/app/data ar-io-core:latestYou can also run Envoy alongside an ar.io node via Docker Compose. Envoy
proxies the Arweave node routes the gateway does not serve itself (such as
POST /tx) to Arweave nodes: by default, public peers it discovers through
ARWEAVE_PEER_DNS_RECORDS (peers.arweave.xyz); with
ENABLE_ARWEAVE_PEER_EDS=false, the static TRUSTED_NODE_HOST (default
arweave.net) and FALLBACK_NODE_HOST. With ARWEAVE_POST_DRY_RUN=true,
Envoy sends POST /tx to the gateway instead, which answers it itself and
broadcasts nothing.
docker compose up --buildOnce running, requests can be directed to Envoy server at localhost:3000.
You can run a Turbo ANS-104 data item bundler as a sidecar to the ar.io gateway service. This will allow the deployed system to accept data items and bundle them into a single transaction before submitting them to the network. The bundler's APIs will be reachable at the /bundler/ path. For more information on its APIs, you can access docs at /bundler/api-docs/.
Note: A local bundler configured to integrate with an ar.io node relies upon GraphQL indexing of recently bundled and uploaded data to manage its pipeline operations. The ar.io node should have its indexes synced up to Arweave's current block height minus 18 blocks before starting up the bundler's services stack.
Bundling services are most easily managed via an independent docker compose file whose services share their network with that of the core services docker compose stack. This allows you to spin the services up when your core service is prepared to integrate with it, or down whenever you want without affecting your core services stack.
To get started, supply the required environment variables in an environment variables file (e.g. .env.bundler) for the integration, most notably:
BUNDLER_ARWEAVE_WALLET: a stringified JWK wallet used for uploading bundles to Arweave.ALLOW_LISTED_ADDRESSES: a comma-separated list of allowed uploader wallet addresses (normalized). See Managing Bundler Access for more permissioning options.
See the .env.bundler.example file for other important configuration options, including settings for serving bundler-uploaded data items from your gateway as soon as they are uploaded.
Once environment variables are set, run docker compose with the bundler-specific compose file.
docker compose --env-file ./.env.bundler --file docker-compose.bundler.yaml upNow, the bundler service will be running alongside the ar.io gateway. Your gateway will now accept data items at <your gateway url>/bundler/tx 🚀
By default, the bundler will only accept data items uploaded by data item signers whose normalized wallet addresses are in the ALLOW_LISTED_ADDRESSES list. But the following other permissioning configuration schemes are possible:
| Scheme | ALLOW_LISTED_ADDRESSES | SKIP_BALANCE_CHECKS | ALLOW_LISTED_SIGNATURE_TYPES | PAYMENT_SERVICE_BASE_URL |
|---|---|---|---|---|
| Allow specific wallets | comma-separated normalized wallet addresses | false | EMPTY or supplied | EMPTY |
| Allow specific chains | EMPTY or supplied | false | arbundles sigtype int | EMPTY |
| Allow all | n/a | true | n/a | n/a |
| Allow none | EMPTY | false | EMPTY | EMPTY |
| Allow payers | EMPTY or supplied | false | EMPTY or supplied | your payment svc url |
When running via docker compose, it will read a .env file in the project root
directory and use the environment variables set there.
Add the following to your .env file to proxy GraphQL to another server while
using the ar.io gateway to serve data (using arweave.net GraphQL as an example):
GRAPHQL_HOST=arweave.net
GRAPHQL_PORT=443
The ar.io gateway supports unbundling and indexing ANS-104 bundle data. To
enable this add the following environment variables to your .env file:
ANS104_UNBUNDLE_FILTER="<filter string>"
ANS104_INDEX_FILTER="<filter string>"
ANS104_UNBUNDLE_FILTER determines which TXs and data items (in the case of
nested bundles) are unbundled, and ANS104_INDEX_FILTER determines which data
items within a bundle get indexed.
The following types of filters are supported:
{ "never": true } # the default
{ "always": true }
{ "attributes": { "owner_address": <owner address>, ... }}
{ "tags": [{ "name": <utf8 tag name>, "value": <utf8 tag value> }, { "name": <utf8 tag name> }, ...]}
{ "and": [ <nested filter>, ... ]}
{ "or": [ <nested filter>, ... ]}
{ "not": [ <nested filter>, ... ]}
Place an ANS-104 bundle at the start of the queue for unbundling and indexing on your gateway:
curl -X POST -H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
"http://<HOST>:<PORT>/ar-io/admin/queue-tx" \
-d '{ "id": "<ID>" }'
Note: ANS-104 indexing support is currently experimental. It has been tested successfully with small sets of bundles (using filters), but you may still encounter problems with it when indexing larger sets of transactions.
For detailed information about filter types, operators, and advanced examples, see Filter Documentation.
The gateway ships with three pre-built CDB64 indexes that resolve data item
IDs to their containing L1 Arweave transactions, up to block height
1,820,000. They cover non-AO, non-Redstone data items with a content type
(about 964 million) and without one (about 1.24 billion), and AO data items
(about 1.63 billion). They are enabled by default, read from Arweave on demand, and
asked last in the default ROOT_TX_LOOKUP_ORDER.
To disable them, remove cdb from ROOT_TX_LOOKUP_ORDER. To use other index
sources, set CDB64_ROOT_TX_INDEX_SOURCES. See
docs/cdb64.md for configuration and operation.
Gateways can also share fresher indexes with each other through Index
Sharing: the optional index-swarm sidecar publishes this gateway's bands,
signed with its observer key, and subscribes to other gateways' bands. Bands
move over the publisher's metered HTTP routes, and, with the optional torrent
engine (compose profile index-swarm-torrent), peer to peer as well, every
subscriber seeding what it installs. Peers announce to a publisher's tracker
through its HTTPS, each index has an RSS feed of its torrents, and each
.torrent names the gateway as a WebSeed, so a stock BitTorrent client can
follow a publisher too. Two kinds of band are shared:
- Root-TX bands (CDB64), which the gateway loads without a restart to
place data items in their root transactions, and to answer
/ar-io/offsets/<id>. - L1 bands (
parquet-l1): the Arweave base layer as Parquet, with lookup files. WithL1_BAND_IMPORT_ENABLEDthe gateway fills its own L1 history from them while it runs, so a new gateway follows the chain tip from the start instead of indexing from height 0. They can also be queried in place with DuckDB.
tools/index-swarm-setup sets it up and tools/index-swarm-status checks
it. See docs/index-swarm.md to run it,
docs/index-export.md to build bands,
docs/index-publication.md for the protocol, and
docs/cli.md for the ar-io-node CLI that builds, checks and
imports bands.
The ar.io gateway includes a feature to emit webhooks to specified servers when a transaction or data item is indexed and matches a predefined filter. This feature allows for real-time notifications and integrations based on the transaction and data indexing.
To use this feature, you need to set up two environment variables in your .env file:
-
WEBHOOK_TARGET_SERVERS: This is a comma-separated list of servers where the webhooks will be sent.
Format:
WEBHOOK_TARGET_SERVERS="<server1>,<server2>,..." -
WEBHOOK_INDEX_FILTER: This filter determines which transactions or data items will trigger the webhook emission.
-
WEBHOOK_BLOCK_FILTER: This filter determines which blocks will trigger the webhook emission.
The filter syntax is identical to ANS104_INDEX_FILTER. Supported filter types include:
{ "never": true }(default){ "always": true }{ "attributes": { "owner": <owner key>, ... }}{ "tags": [{ "name": <utf8 tag name>, "value": <utf8 tag value> }, { "name": <utf8 tag name> }, ...]}{ "and": [ <nested filter>, ... ]}{ "or": [ <nested filter>, ... ]}
Example: WEBHOOK_INDEX_FILTER="{ "tags": [{ "name": "App-Name", "value": "MyApp" }, { "name": "IPFS-Add" }]}"
After setting up the environment variables, the ar.io gateway will monitor for transactions or data items that match the WEBHOOK_INDEX_FILTER. Once a match is found, a webhook will be emitted to all the servers listed in WEBHOOK_TARGET_SERVERS.
Ensure that the target servers are configured to receive and process these webhooks appropriately.
For detailed information about filter types, operators, and advanced examples, see Filter Documentation.
Add the following to your .env file to enable ArNS resolution:
ARNS_ROOT_HOST=<gateway-hostname>
For example if your gateway's hostname was my-gateway.net your .env would
contain the following:
ARNS_ROOT_HOST=my-gateway.net
This would allow you to resolve names like my-arns-name.my-gateway.net provided
you correctly configured a wildcard DNS entry for your gateway.
ArNS names resolve in the order ARNS_RESOLVER_PRIORITY_ORDER sets, by
default on-demand,gateway: first from the AR.IO Network's Solana programs
directly, then from a trusted gateway (TRUSTED_ARNS_GATEWAY_URL, default
https://__NAME__.turbo-gateway.com).
In order to participate in the ar.io network, gateways need
to associate themselves with a wallet. This can be configured by setting the
AR_IO_WALLET environment variable. Once set, the associated wallet address is
visible via the /ar-io/info endpoint.
Similarly, network participants must make observations of other gateways and
submit them. The wallet for this is configured using the OBSERVER_WALLET
environment variable. An associated key file is also required to upload
observation reports. The key file must be placed in
./wallets/<OBSERVER_WALLET>.json (<OBSERVER_WALLET> should be replaced with
the address of the wallet you are using).
HTTP endpoints under '/ar-io/admin' are protected by an admin API key. On startup,
the admin key is read from the ADMIN_API_KEY environment variable. If no key is
set, a random key is generated and logged. To make a request to an admin endpoint
add an Authorization: Bearer <ADMIN_API_KEY> header to your request.
Block a specific TX/data item ID on your gateway:
curl -X PUT -H "Authorization: Bearer <ADMIN_KEY>" \
-H "Content-Type: application/json" \
"http://<HOST>:<PORT>/ar-io/admin/block-data" \
-d '{ "id": "<ID>", "notes": "Example notes", "source": "Example source" }'
notes and source are for documentation only. source is intended to be an
identifier of a particular source of IDs to block (e.g. the name of a
blocklist). notes is a text field that can be used to further describe why a
particular ID is blocked.
The ar-io-node uses Prometheus to collect metrics from the system and recommends Grafana to visualize them. To access a templated Grafana dashboard for the ar.io gateway, you can run:
docker compose --file docker-compose.grafana.yaml up -d
Once the dashboard is running, you can access it at
http://localhost:1024/grafana and login with the username and password
admin.
This dashboard is pre-configured to work with the ar.io gateway metrics exposed
via the ar-io-core service and is ready to be used without any additional
configuration for simple observability. You can modify the dashboard to better
fit your needs by editing the dashboard.json file. Refer to the Grafana
documentation to learn more about how to create and modify Grafana dashboards
using JSON model files.
The ar-io-node uses OpenTelemetry for distributed tracing, providing deep visibility into request flows and performance bottlenecks. When deployed with docker-compose, traces are automatically routed through an OTEL Collector sidecar that implements intelligent tail-based sampling to reduce telemetry costs by 80-95% while maintaining complete visibility into errors and performance issues.
How Tail Sampling Works:
Unlike traditional head-based sampling (which decides whether to keep a trace at the start), tail sampling waits until the entire trace completes before making a decision. This enables five intelligent sampling policies:
- 100% Error Capture: All traces with errors (5xx responses, exceptions) are captured
- 100% Slow Request Capture: All requests exceeding 2 seconds (configurable) are captured
- 100% Payment Capture: All traces with verified x402 payments are captured for billing and compliance
- 100% Paid Token Capture: All traces that consumed paid rate limit tokens are captured
- Probabilistic Baseline: 1% (configurable) of successful, fast, unpaid requests for baseline metrics
This approach ensures you never miss critical failures, paid traffic, or compliance-relevant requests while dramatically reducing storage and analysis costs for free-tier traffic.
Configuration:
To use OTEL Collector with tail sampling in docker-compose:
-
Set your telemetry destination and API key (configure ONE based on your backend):
# Honeycomb OTEL_COLLECTOR_DESTINATION_ENDPOINT=https://api.honeycomb.io OTEL_COLLECTOR_HONEYCOMB_API_KEY=your-honeycomb-api-key # OR Grafana Cloud Tempo OTEL_COLLECTOR_DESTINATION_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp OTEL_COLLECTOR_GRAFANA_CLOUD_API_KEY=your-base64-encoded-key # OR Datadog OTEL_COLLECTOR_DESTINATION_ENDPOINT=https://trace.agent.datadoghq.com OTEL_COLLECTOR_DATADOG_API_KEY=your-datadog-api-key # OR New Relic OTEL_COLLECTOR_DESTINATION_ENDPOINT=https://otlp.nr-data.net OTEL_COLLECTOR_NEW_RELIC_API_KEY=your-new-relic-license-key # OR Elastic APM OTEL_COLLECTOR_DESTINATION_ENDPOINT=https://your-deployment.apm.region.cloud.es.io OTEL_COLLECTOR_ELASTIC_API_KEY=your-elastic-secret-token
-
Optionally adjust sampling parameters:
# Sample 5% of successful requests instead of 1% OTEL_TAIL_SAMPLING_SUCCESS_RATE=5 # Capture requests slower than 1 second instead of 2 OTEL_TAIL_SAMPLING_SLOW_THRESHOLD_MS=1000
-
Start the stack:
docker compose up -d
The collector runs automatically as part of the docker-compose stack. Traces flow through: ar-io-node → otel-collector → your telemetry backend.
Non-Docker Deployments:
For non-docker deployments, you can send traces directly to your telemetry backend by setting:
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=your-api-keyNote: Direct export bypasses tail sampling and uses head-based sampling via OTEL_TRACING_SAMPLING_RATE_DENOMINATOR.
Supported Telemetry Backends:
The OTEL Collector works with any OTLP-compatible backend:
- Honeycomb
- Grafana Cloud (Tempo)
- Datadog
- New Relic
- Elastic APM
- Self-hosted: Jaeger, Zipkin, Tempo
For complete configuration options, see the environment variables documentation.
The AR.IO Node supports filtering Winston logs with the LOG_FILTER
environment variable (or CORE_LOG_FILTER when using docker-compose). This
helps reduce log noise by showing only logs that match specific criteria.
Filter logs using the same JSON-based filtering syntax used for bundle filters, with the filter logic applying to log attributes instead of bundle attributes:
# Only show logs from ArNS resolvers
LOG_FILTER={"attributes":{"class":"CompositeArNSResolver"}}
# Only show logs from the on-demand ArNS resolver
LOG_FILTER={"attributes":{"class":"OnDemandArNSResolver"}}
# Show logs from multiple ArNS components using logical operators
LOG_FILTER={"or":[{"attributes":{"class":"CompositeArNSResolver"}},{"attributes":{"class":"OnDemandArNSResolver"}}]}
For more details on filter syntax, see the filter documentation - while this documentation focuses on bundle filters, the filter structure and logic apply to log filtering as well.
Diagrams of the gateway, its chain, data and GraphQL paths are in
docs/diagrams/, with their PlantUML sources under
docs/diagrams/src/.
- Code to interfaces.
- Separate IO from application logic.
- Make processes idempotent whenever possible.
- Separate mutable from immutable data.
- Avoid trusting data when the cost to validate it is low.
- To support rapid development iteration, All system components must be runnable in a single process.
- Keep the compile test suite blazingly fast.
- In general, prefer in-memory implementations over mocks and stubs.
- In general, prefer sociable over solitary tests.
- Commit messages should describe both what is being changed and why it is being changed.
- Make liberal use of Prometheus metrics to aid in monitoring and debugging.
- Follow the Prometheus metrics naming recommendations.