Apollo Client
apollographql/skills
Guide for building React applications with Apollo Client 4.x.
Operate any AR.IO node deployment — architecture, daily ops, diagnostics, and recurring pitfalls that apply to every operator.
$ npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ar-io/ar-io-node ar-io-gateway-operator --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/ar-io/ar-io-node.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/ar-io-gateway-operator .claude/skills/ar-io-gateway-operator && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "ar-io-gateway-operator" agent skill from https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operator into .claude/skills/ar-io-gateway-operator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ar-io-gateway-operator", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operatorType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ar-io/ar-io-node ar-io-gateway-operator --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ar-io/ar-io-node.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/ar-io-gateway-operator .agents/skills/ar-io-gateway-operator && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "ar-io-gateway-operator" agent skill from https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operator into .agents/skills/ar-io-gateway-operator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ar-io-gateway-operator", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ar-io/ar-io-node ar-io-gateway-operator --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ar-io/ar-io-node.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/ar-io-gateway-operator .cursor/skills/ar-io-gateway-operator && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "ar-io-gateway-operator" agent skill from https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operator into .cursor/skills/ar-io-gateway-operator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ar-io-gateway-operator", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ar-io/ar-io-node.git --path .claude/skills/ar-io-gateway-operator--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ar-io/ar-io-node ar-io-gateway-operator --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ar-io/ar-io-node.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/ar-io-gateway-operator .gemini/skills/ar-io-gateway-operator && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "ar-io-gateway-operator" agent skill from https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operator into .gemini/skills/ar-io-gateway-operator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ar-io-gateway-operator", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ar-io/ar-io-node ar-io-gateway-operatorInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ar-io/ar-io-node.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/ar-io-gateway-operator .github/skills/ar-io-gateway-operator && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "ar-io-gateway-operator" agent skill from https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operator into .github/skills/ar-io-gateway-operator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ar-io-gateway-operator", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ar-io/ar-io-node ar-io-gateway-operator --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ar-io/ar-io-node.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/ar-io-gateway-operator .opencode/skills/ar-io-gateway-operator && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "ar-io-gateway-operator" agent skill from https://github.com/ar-io/ar-io-node/tree/develop/.claude/skills/ar-io-gateway-operator into .opencode/skills/ar-io-gateway-operator/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ar-io-gateway-operator", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
ar-io-gateway-operatorOperate any AR.IO node deployment — architecture, daily ops, diagnostics, and recurring pitfalls that apply to every operator.
Ar Io Gateway Operator is an agent skill from ar-io/ar-io-node. Operate any AR.IO node deployment — architecture, daily ops, diagnostics, and recurring pitfalls that apply to every operator. Use whenever the user asks about gateway health, indexing lag, ClickHouse pipeline, GraphQL routing, ArNS resolution, data retrieval and caching, bundle unbundling, webhook fan-out, container restarts, image rebuilds, disk pressure, HTTPSig overhead, or anything operationally generic to running an AR.IO node — even when they don't name a specific service ("is it healthy?", "why is it…
Its SKILL.md is about 8.8k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files, including scripts.
It sits in Backend & APIs, covering GraphQL, Data warehousing and Deployment. It works with GraphQL, ClickHouse, SQLite and TypeScript. The repository describes itself as: A scalable and modular gateway built for the permaweb atop the Arweave permanent data storage network. The licence is AGPL-3.0.
12 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit aee1c67. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 1 file in scripts/, which the agent can run.
Shell commands in SKILL.md call:
dockercurlnpmFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
turbo-gateway.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
ADMIN_API_KEYSOLANA_PRIVATE_KEYOBSERVER_PRIVATE_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Ar Io Gateway Operator loads about 8.8k tokens when it runs. Until then it costs about 195 tokens; SKILL.md has 4,263 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
Configurable via `.env`. Defaults:ng's working" report is a gateway whose `.env` still pins the old network's program IDs.wallet> --torrent --dry-run` shows the `.env` changes (subscription, CDB64 sources, lookup order, engine password); `--sudo journalctl --vacuum-size=500M # if /var/log/journal is largeADMIN=$(grep '^ADMIN_API_KEY=' .env | cut -d= -f2-)after a migration / image swap** — the `.env` has explicit `CORE_IMAGE_TAG` / `ENVOY_IMAGE_TAG` / `OBSERVER_IMAGE_TAG`401/404 | `ADMIN_API_KEY` mismatch with `.env` |t | `WEBHOOK_TARGET_SERVERS` in gateway `.env`; sidecars must be on the same docker network and reachable by service namws wrong `programIds` after migration | `.env` likely has explicit `*_IMAGE_TAG` lines pinning the old images. Update thAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from ar-io/ar-io-node at commit aee1c67, republished under its AGPL-3.0 licence (© ar-io). 4,263 words, ~8,837 tokens.
.claude/skills/ar-io-gateway-operator/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.You are operating an AR.IO node — an Arweave gateway that indexes, caches, serves, and signs blockchain data. Deployments share the same architecture but differ in domains, paths, and which sidecars are present. Run scripts/health-check first every session: it prints a one-screen snapshot you can compare against the prior state before deciding to act.
If a deployment-specific overlay skill is loaded (e.g. vilenarios-gateway), it provides the local repo paths, domains, and which sidecars exist. Treat the overlay as authoritative for those facts.
scripts/health-check# from the ar-io-node repo root
.claude/skills/ar-io-gateway-operator/scripts/health-checkWhat it checks and why:
| Section | What "good" looks like | If wrong, jump to |
|---|---|---|
| containers | All core ar-io-node containers Up | "Bring everything up" |
| disk | / < 80%, /data (if separate) has headroom | "Disk pressure" |
| gateway healthcheck | {"status":"ok",...} | "When something breaks" |
| indexer height vs chain head | lag ≤ 5 blocks | "Indexing falling behind" |
| ClickHouse rows | If streaming enabled: new_blocks > 0 and growing | "Streaming tables empty" |
| core errors last 5 min | < ~10 (operational noise: peer fetches, aborts) | grep deeper |
| image SHAs | If on a dev branch, must differ from :latest digest | "Building from source" |
The script only covers the core ar-io-node containers defined in this repo's docker-compose. Sidecar checks belong in a deployment-overlay skill that calls this script then adds its own checks on top.
Knowing the data flows tells you where to look when symptoms appear. Source of truth is src/system.ts (DI wiring) and docs/; this section captures the operationally-load-bearing facts.
client → (optional edge: nginx/CDN) → envoy :3000 → core :4000 → response with X-AR-IO-* trust headers
↘ webhook fan-out → optional sidecarsEvery served data response carries trust headers (X-AR-IO-Verified, X-AR-IO-Trusted, X-AR-IO-Stable, X-AR-IO-Hops, plus X-Arweave-* tag passthrough). Those headers are what HTTPSig signs — see src/middleware/httpsig.ts TRIGGER_HEADERS. Endpoints with no trust trigger (/ar-io/info, /ar-io/healthcheck, /graphql errors) are deliberately not signed.
Configurable via .env. Defaults:
ON_DEMAND_RETRIEVAL_ORDER=trusted-gateways,ar-io-network,chunks-offset-aware,tx-data — what /raw/<id> and /<id> walk through on a cache missBACKGROUND_RETRIEVAL_ORDER=chunks — used by the unbundler when fetching bundle bodiesEach source's failures are logged with class so you can grep (GatewaysDataSource, TxChunksDataSource, ChunksDataSource, etc.). Persistent failures from one tier are normal — the chain is designed to fall through. Look for Failed to fetch chunk data from any source / all sources failed as the real failure signal, not individual tier warnings.
Three response headers worth knowing: X-Cache: HIT|MISS (gateway-side contiguous cache), X-AR-IO-Hops (how many AR.IO peers were traversed before this gateway served), X-AR-IO-Trusted / X-AR-IO-Verified (whether the data has been hash-verified against the chain).
Two write paths run side-by-side:
1. SQLite primary indexer (always on) — BlockImporter worker imports blocks one at a time into new_blocks/new_transactions (the unstable head, ~last 18 blocks), and a separate process promotes them to stable_blocks/stable_transactions once they're past the reorg horizon. ANS-104 bundle unbundling is gated by ANS104_UNBUNDLE_FILTER (often narrow). Database access is in a worker thread (StandaloneSqlite) — never call SQLite synchronously from the main thread.
2. ClickHouse pipeline (optional, --profile clickhouse) — Stable rows are exported to parquet then imported by clickhouse-auto-import (hourly cycles) into staging_* and finally transactions. Streaming pipeline (PR #699 / clickhouse-streaming-unstable-head branch) mirrors the unstable-head SQLite writes directly into ClickHouse new_blocks / new_transactions so GraphQL can query the head without waiting for the parquet round-trip. Activated by CLICKHOUSE_STREAMING_ENABLED=true. Reorgs trigger bounded ALTER TABLE ... DELETE WHERE on the new_* tables.
/graphql reads from up to three legs that get merged: stable transactions table in CH, streaming new_transactions in CH, and the SQLite new_* fallback. With CLICKHOUSE_GQL_SKIP_SQLITE_READS=true, the SQLite leg is dropped to a tight-timeout fallback only. With GRAPHQL_ON_DEMAND_RESOLUTION_ENABLED=true (default), transaction(id) queries that miss the local DB will fall back to extracting from ANS-104 bundle binaries on demand — single-ID only, capped by GRAPHQL_ON_DEMAND_RESOLUTION_MAX_CONCURRENT.
Whenever a bundle TX matches ANS104_UNBUNDLE_FILTER, it walks a four-queue chain. Knowing the chain is essential for diagnosing throughput regressions — a single queue at cap can mask everything downstream of it.
bundleDataImporter ─→ ans104Unbundler ─→ ans104DataIndexer ─→ dataItemIndexer ─→ SQLite writer
download workers parser workers post-parse buffer pre-write buffer single-writer
(ANS104_DOWNLOAD_…) (ANS104_UNBUNDLE_) (ANS104_DATA_…_SIZE) (DATA_ITEM_…_SIZE)Tune knobs (all env-overridable):
| Knob | Default | What it controls |
|---|---|---|
ANS104_DOWNLOAD_WORKERS | 5 | Parallel bundle downloads. Network-bound; overprovisioning is cheap |
ANS104_UNBUNDLE_WORKERS | 1 | Parallel parser worker_threads. CPU + network bound |
DATA_ITEM_INDEXER_WORKER_COUNT | 1 | Fastq concurrency for indexDataItem; SQLite is single-writer so gains are bounded |
MAX_DATA_ITEM_QUEUE_SIZE | 100000 | Soft gate: shouldUnbundle() returns false above this on either indexer queue |
DATA_ITEM_INDEXER_QUEUE_SIZE | 500000 | Hard cap on dataItemIndexer; items past this get dropped silently |
ANS104_DATA_INDEXER_QUEUE_SIZE | 500000 | Hard cap on ans104DataIndexer |
The buffer between soft gate and hard cap is where data loss happens. With defaults the gate engages at 100 K but the hard cap is 500 K — a 5× buffer. Under heavy load, in-flight parser workers can flood items into the queue after the gate engages but before the indexer drains, exceeding the hard cap and silently dropping items. Symptom: data_items_dropped_total{queue_name="dataItemIndexer"} climbs while bundles_unbundled_total keeps increasing — bundles look "indexed" but their items aren't searchable.
Mitigations:
MAX_DATA_ITEM_QUEUE_SIZE so the gate engages well before the hard cap (e.g., 25 000 with 500 000 hard caps, giving a 20× buffer).data_items_dropped_total is non-zero — more workers = bigger floods past the gate.Pipeline diagnostic: a single curl -sf .../ar-io/__gateway_metrics | grep -E 'queue_length|data_items_dropped|bundles_unbundle_in_flight' tells you which queue is the bottleneck.
| Layer | Where | TTL | Notes |
|---|---|---|---|
| In-process kv | core memory | varies per resolver | ArNS names cache, etc. |
| Redis | redis:7 container | varies | Some ArNS / network state |
| Contiguous data store | ${CONTIGUOUS_DATA_PATH}/ | unbounded | Files keyed by content hash; cleanup workers prune |
| Response cache hints | Cache-Control headers in response | per-route | e.g., /raw/:id HIT serves with max-age=43200 |
| Edge cache (optional) | nginx/Varnish/CDN in front | per-zone | Only present if the operator deployed one — gateway doesn't run one itself |
X-Cache: HIT on a /raw/<id> response means the gateway's contiguous data store served bytes without going to peers/Arweave. The edge layer is separate — nginx's X-Cache-Status: HIT is a different layer.
CompositeArNSResolver walks resolvers in order: TrustedGatewayArNSResolver (asks TRUSTED_ARNS_GATEWAY_URL, default https://__NAME__.turbo-gateway.com), then OnDemandArNSResolver (queries the on-chain ario-arns and ario-ant programs directly via SOLANA_RPC_URL). The base name set is paginated into an in-memory ArNSNamesCache at boot and refreshed on a debounce. Resolved IDs may be Arweave TXs (route to data path) or, on the streaming-head branch, IPFS CIDs (route to /ipfs/<cid> via the Kubo sidecar). Unknown names log Unable to resolve name against all resolvers — that's normal user-error traffic, not a service failure.
/<name> and /<name>/<path> requests carry X-ArNS-* trust headers in the response. Manifest path resolution still uses StreamingManifestPathResolver; the "from index" path is not implemented yet (logs warn not implemented then falls back to data-side resolution, which works).
A gateway is a registered participant in the AR.IO network, not just a piece of software. Two distinct Solana identities matter:
AR_IO_WALLET (pubkey, base58) plus a signing key as either SOLANA_KEYPAIR_PATH (JSON file) or SOLANA_PRIVATE_KEY (base58 secret, Phantom-export form). Stakes ARIO tokens (20,000 minimum = 20000000000 mARIO since v3.0.0), receives rewards, signs cranker instructions when ENABLE_EPOCH_CRANKING=true.OBSERVER_WALLET (pubkey) plus a signing key as either OBSERVER_KEYPAIR_PATH or OBSERVER_PRIVATE_KEY. Submits save_observations instructions and signs HTTPSig responses. The two roles are deliberately separable so the observer key can sit on the gateway box while the operator/staking key stays in cold storage. Setting both forms (path and PK string) for the same role is rejected at startup.The Solana key designated for the observer role is the HTTPSig signing key. The observer's address is already registered in the on-chain Gateway Registry (GAR), so verifiers can confirm a signed response traces back to a staked, registered gateway with a single GAR lookup. No attestation document is uploaded to Arweave on the Solana path — the chain is the source of truth.
Four Solana program IDs configure which network the gateway talks to: ARIO_CORE_PROGRAM_ID, ARIO_GAR_PROGRAM_ID, ARIO_ARNS_PROGRAM_ID, ARIO_ANT_PROGRAM_ID. Plus SOLANA_RPC_URL (premium provider strongly recommended in production; public mainnet-beta throttles hard). GET /ar-io/info echoes the resolved programIds object, which is the fastest way to confirm a running gateway is pointed at the network you think it is.
Observer service knobs:
RUN_OBSERVER=true (default) — runs the observer alongside the gatewaySUBMIT_CONTRACT_INTERACTIONS=true (default) — submits generated reports on-chainOBSERVATIONS_PER_GATEWAY (default 3) — how many times each gateway is observed per epoch (majority vote)OBSERVATION_WINDOW_FRACTION (default 0.5) — what fraction of the daily epoch window observations are spread acrossREFERENCE_GATEWAY_MIN_CONSECUTIVE_PASSES / REFERENCE_GATEWAY_MIN_EPOCH_COUNT — eligibility thresholds for using a network gateway as a referenceEpoch cadence is daily. Observation reports drive reward eligibility. Operators may themselves be selected as observers for other gateways.
Network onboarding (operator side, only needed once):
npm install -g @ar.io/sdk — provides the ar.io CLI binary. Use ar.io get-gateway --address <pubkey> to read state, ar.io join-network ... to register.--wallet-file <path> (JSON) or --private-key '<base58>' (Phantom export — bs58.decode runs in-process).-t solana and the four program IDs (--core-program-id / --gar-program-id / --arns-program-id / --ant-program-id) plus --rpc-url. As of @ar.io/sdk@^4.0.0-solana.22 the bundled "mainnet" constants are still placeholders, so passing --mainnet alone resolves to non-existent addresses — always pass the explicit program IDs the network team publishes for whichever environment you're targeting (mainnet, staging-devnet, local devnet).--operator-stake 20000 in whole ARIO — the CLI converts to mARIO internally via requiredMARIOFromOptions → new ARIOToken(...).toMARIO()).programIds set you joined with. The usual cause of a "I joined but nothing's working" report is a gateway whose .env still pins the old network's program IDs.--min-delegated-stake is mARIO directly (no ARIO→mARIO conversion); --operator-stake is the only stake-input flag that takes whole ARIO.Two mutually-relevant env vars decide what / serves:
APEX_TX_ID — pin a specific Arweave TX as the apex. Cache-Control bounded by CACHE_APEX_MAX_AGE with must-revalidate so apex rotations propagate.APEX_ARNS_NAME — resolve an ArNS name as the apex. Comma-separated values position-map to ARNS_ROOT_HOST entries when there's more than one host.ARNS_ROOT_HOST — the gateway's primary domain(s) for ArNS subdomain resolution.The index-swarm sidecar (compose profile index-swarm) publishes this gateway's CDB64 root-TX index bands, signed with the observer key, and subscribes to other gateways' bands, installing them under data/indexes/installed/ where the gateway loads them without a restart. It resolves publishers through the gateway's own /ar-io/peers, so it makes no RPC calls. The gateway serves /ar-io/indexes (the signed document) and the byte routes, metered like data. The optional torrent engine (profile index-swarm-torrent; setting INDEX_SWARM_ENGINE_AUTH turns it on) adds a BitTorrent tier: peers first, the HTTP routes as fallback, every subscriber seeding what it installs. The engine runs on its own Docker network and filters private addresses; on a LAN-only swarm set INDEX_SWARM_ENGINE_BLOCK_PRIVATE=false. Health: index_subscription_manifest_age_seconds (publisher gone quiet), index_swarm_installed_bands, index_subscription_bytes_total{transport}, index_swarm_engine_available. On a publisher, who shares: index_swarm_tracker_seeding_hosts (other hosts seeding a band) and index_swarm_tracker_seeders. Full guide: docs/index-swarm.md.
A fleet behind a load balancer (docs/index-swarm.md, "Publishing torrents from a fleet"): only the node /ar-io/indexes* is pinned to publishes and runs the engine and tracker; its peer port goes to its own public IP, not through the LB (set INDEX_SWARM_ENGINE_PUBLIC_HOST), and the tracker is direct or /announce proxied with INDEX_SWARM_TRACKER_TRUSTED_PROXIES. Rate limiter and x402 need nothing new: byte routes and the WebSeed are metered, the document and .torrent are free. Other nodes that answer lookups subscribe over HTTP with url pointed at the publishing node, allowlisted in its RATE_LIMITER_IPS_AND_CIDRS_ALLOWLIST.
Ports and disk. The engine's peer port (INDEX_SWARM_ENGINE_PORT, 6881 TCP+UDP) and the tracker's (INDEX_SWARM_TRACKER_PORT, 6969 TCP) are Docker-published, so ufw neither blocks nor protects them (filter where Docker forwards: the DOCKER-USER chain with Docker's default iptables backend; with its nftables backend, which has no DOCKER-USER, a chain in your own table on the forward hook at a priority before Docker's), and a port already held on the host stops the container starting. A publisher needs its peer port reachable, and the tracker port only when its torrents announce to it directly (INDEX_SWARM_TRACKERS naming this node's port): not when /announce is proxied through a load balancer, nor when it publishes with no tracker (it warns, and peers then meet only through DHT and the WebSeed). A subscriber needs neither (behind NAT it still downloads and seeds to peers it dials), and index-swarm-status reports a closed port as WARN, not FAIL. The engine has UPnP off. INDEX_SWARM_MAX_DISK_BYTES (50 GiB from the setup script) is a ceiling, not a reservation: nothing checks free space, so a full disk under data/indexes is the operator's to prevent. That directory must be one filesystem (renames, hard links); move it whole with INDEX_SWARM_DATA_PATH. Upload defaults to 10 MB/s and 100 GB a UTC day, up to about 3 TB a month.
Build and check bands with ./tools/ar-io-node (docs/cli.md, "For scripts and agents"): index-band-build (CSV records in, header check against --gateway-url, published into data/indexes/published/root-tx-index) and index-band-verify. JSON on stdout, exit 1 on failure, safe to rerun (an identical band reports "unchanged": true). Point --gateway-url at a gateway that holds the roots (https://turbo-gateway.com); a gateway that must fetch them times out and the band is refused as under-checked, not wrong. Any other command runs as the SDK's ar.io CLI.
Bands are built by the index-export service (compose profile index-export; docs/index-swarm.md, "Producing bands"), once a day at INDEX_EXPORT_RUN_AT_UTC from INDEX_EXPORT_SOURCES (this gateway's ClickHouse, else SQLite; peers and a bundler overlay merge). It needs INDEX_EXPORT_START_HEIGHT for its first run and INDEX_EXPORT_HEADER_CHECK_URL always (a gateway holding the roots; not core:4000 on a gateway that fetches them). Dry-run before starting: docker compose --profile index-export run --rm index-export --once --dry-run. Bring it up by name with --no-deps. Its /healthz is loop liveness only; outcomes are index_export_runs_total{result,reason} and index_export_last_success_timestamp_seconds{kind="d"} (alert past 2 days). couldnt_check retries itself (15 min doubling to 2 h); rejected (a wrong header, conflicts over 1%) never does and needs a look. A stale data/indexes/export/lock (5 min untouched) is cleared by the next run; state.json there is re-derivable except adoptions. With parquet-l1 in INDEX_EXPORT_KINDS it also builds L1 bands from core.db, and adds lookup files (layout l1-3) to published L1 bands in place, keeping their ids (index_export_runs_total{kind="derive"}). Upgrade a gateway that subscribes to parquet-l1 before its publisher writes l1-3 (an older one refuses those bands, naming the layout, and keeps its copies), and a publisher's index-swarm with or before its index-export (an older sidecar can't describe an l1-3 band, so it goes unpublished).
Set up and check with the scripts, not by hand: ./tools/index-swarm-setup --subscribe <publisher wallet> --torrent --dry-run shows the .env changes (subscription, CDB64 sources, lookup order, engine password); --restart applies them, recreating only what needs it with the running gateway's compose files. ./tools/index-swarm-status is the first thing to run when asked whether index sharing works: read-only, ok/WARN/FAIL per check with the fix, exit 1 on a failure.
Three filters compose JSON expressions (tags, attributes, or/and/always/never) — see docs/filters.md:
ANS104_UNBUNDLE_FILTER — which bundles get unbundledANS104_INDEX_FILTER — which data items inside matched bundles get persistedWEBHOOK_INDEX_FILTER — which indexed items trigger webhook emissionWebhook events fired: data-cached, tx-indexed, ans104-data-item-indexed. WEBHOOK_TARGET_SERVERS is a comma-separated list of URLs that receive POSTs. If you change a filter to broaden indexing, expect proportionally higher webhook volume.
# from the ar-io-node repo root
docker compose --profile clickhouse up -dThe --profile clickhouse flag matters: without it, ClickHouse and the auto-import container stay down and the streaming pipeline goes silent. Other profiles (grafana, bundler, litestream, ao, hb) gate their own services.
Wait for ready:
until curl -sf http://localhost:4000/ar-io/healthcheck >/dev/null; do sleep 2; donedocker compose --profile clickhouse build core clickhouse-auto-import
docker compose --profile clickhouse up -d --force-recreate core clickhouse-auto-import--force-recreate is required — without it, compose sees the same image name (:latest) and skips recreation even though the image digest changed. Confirm with docker inspect <container> --format '{{.Image}}' — the SHA must differ from before.
docker system df # see what's hogging
docker builder prune -af # build cache (almost always biggest)
docker container prune -f # stopped containers
docker image prune -af # untagged images
sudo journalctl --vacuum-size=500M # if /var/log/journal is largeAvoid docker volume prune -af without listing first (docker volume ls -f dangling=true) — there can be old SQLite/CH data in dangling volumes worth keeping.
ADMIN=$(grep '^ADMIN_API_KEY=' .env | cut -d= -f2-)
TX=<arweave-tx-or-ipfs-cid>
# Block (the gateway has PUT only — there is no DELETE handler today)
curl -sf -X PUT "http://localhost:4000/ar-io/admin/block-data" \
-H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
-d "{\"id\":\"$TX\",\"hash\":\"$TX\",\"source\":\"manual\",\"notes\":\"...\"}"If a moderation sidecar is present (and wired via WEBHOOK_TARGET_SERVERS), prefer routing the block through it — depending on the sidecar's config, it may also bust an edge cache after blocking.
Read the in-process histogram for the truest per-request signing cost — no benchmarking needed:
curl -sf http://localhost:4000/ar-io/__gateway_metrics | grep '^httpsig_signing_duration'
# mean per sign = sum / count (typically 70-120 µs; under 500 µs at p100)Don't draw conclusions from /ar-io/info or /ar-io/healthcheck benchmarks — those carry no trust-trigger headers and are never signed.
clickhouse-auto-import logs Failed to connect to core port 4000 after 0 ms — the script's connection state goes stale when core is recreated mid-cycle, and auto-import sleeps 1 h between cycles, so the bad state lingers. Recreating just the auto-import container clears it; manual curl http://core:4000/... from inside the container will succeed even while the script is failing.
Streaming tables empty (new_blocks / new_transactions = 0) despite CLICKHOUSE_STREAMING_ENABLED=true — running the published :latest core image instead of a local build. The streaming columns/tables are populated only by the dev-branch code. Rebuild + force-recreate.
Webhook error flood (getaddrinfo ENOTFOUND <sidecar-host>) — a configured sidecar isn't running. Bring it up. Don't strip the target unless intentionally retiring the sidecar — those targets are how moderation and provenance subsystems work.
Bundle unbundling looks idle (low matched count) — ANS104_UNBUNDLE_FILTER is intentionally narrow on most operator configs (e.g., one App-Name + one owner). Confirm via /ar-io/info. Not a bug.
Gateway has no DELETE /ar-io/admin/block-data handler — only PUT. Sidecars that send DELETE (e.g. an unblock action) will see failures. Document but don't try to "fix" by changing the sidecar — the gateway is the side that needs the route.
/ar-io/info and /ar-io/healthcheck are unsigned — they carry no trust-trigger headers, so HTTPSig signing skips them. Use /raw/<id> against a cached tx for HTTPSig benchmarks.
docker logs <core> | tail can dump megabytes of binary — the streaming pipeline sometimes logs cert chains as raw byte arrays. Filter with grep -viE '"buffer":|raw bytes|^[\[0-9, ]+$'.
Five-edit dance for adding a database method — SQL statement, worker impl in StandaloneSqlite, queue wrapper in main DB class, case in worker message handler, interface in types.d.ts. Skip any one and you get silent failures.
Observer restart-loops with Epoch 0 PDA not found at <pda> — has prescribe_epoch run yet? — the network you're pointed at hasn't had its first epoch initialized. The observer can't bootstrap without entropy from epoch[N].prescribed_observers, and that PDA only exists after a cranker has called create_epoch. Confirm with ar.io get-current-epoch ... — "Epoch 0 not found" means network-side, not gateway-side. Wait for an active cranker (yours or another operator's) to bootstrap epoch 0.
Cranker started cleanly but never logs any [crank:*] activity — EpochSettings.enabled === false on the target network. The cranker bails silently at epoch-cranker.ts:173 (debug log only) so it doesn't burn SOL submitting ix against a paused network. Verify with ar.io get-epoch-settings .... No fix on the gateway side; this is a network-operations state.
ArNS names return 404 even though ar.io get-arns-record --name <name> returns the record fine — SDK version drift. ArNSNamesCache.hydrate paginates the on-chain registry; an SDK pin that's significantly older than the deployed ario-arns program may parse paginated responses incorrectly, hydrating "successfully" but with most entries missing. Logs show Successfully hydrated ArNS names cache quickly (~few seconds for thousands of records) followed by Base name not found in ArNS names cache on lookups. Fix: bump @ar.io/sdk in package.json to the latest ^4.0.0-solana.* and rebuild.
/ar-io/info shows the OLD network's programIds after a migration / image swap — the .env has explicit CORE_IMAGE_TAG / ENVOY_IMAGE_TAG / OBSERVER_IMAGE_TAG pins that shadow the compose defaults. Updating only the compose defaults won't move a gateway whose .env still pins old image tags. Either update both, or remove the explicit pins from .env and let compose defaults win.
Index bands installed but lookups still go to the network — ROOT_TX_LOOKUP_ORDER must put cdb right after db, and CDB64_ROOT_TX_INDEX_SOURCES must start with data/indexes/installed/root-tx-index; both need a gateway restart. Bring them up by service name: the sidecar alone with docker compose --profile index-swarm up -d index-swarm, and with the torrent engine docker compose --profile index-swarm --profile index-swarm-torrent up -d index-swarm index-swarm-engine (its init runs first by dependency). A bare up with a profile starts every default service too.
Every visitor shares one rate-limit bucket, or all get 429/402 together — the gateway believes X-Forwarded-For only from TRUSTED_PROXIES (default: loopback and private ranges). Behind a CDN or a load balancer on public addresses, the CDN's own address is then taken as the client. Add its published ranges to TRUSTED_PROXIES (keep the private defaults if nginx sits in between) and recreate core. Never set TRUSTED_PROXIES=none with the standard compose: every request through Envoy (port 3000) would carry Envoy's address and share one bucket. none is only for a core that clients reach directly. Allowlists match the client address only, never other addresses in its headers.
A range request on an index band file returns 200 with the whole file, and the next one returns 402 - a proxy in front has a proxy_cache zone covering /ar-io/indexes. When a location has a cache zone, nginx strips the client's Range from the upstream request and fetches the whole object so it can store it, which happens before it sees any Cache-Control, so no response header can prevent it and proxy_buffering off alone does not fix it. Whether the client sees the full body depends on whether the response was cacheable: a metering gateway sends private, so nothing is stored and nginx returns 200 with everything on every request, while a gateway that does not meter sends public and nginx answers the range from cache, appearing to work while still pulling each whole file once and filling the zone with band files of up to 1.35 GB. Use proxy_cache off for the prefix (no trailing slash: with one, nginx answers the unslashed form with a 301, and the document at /ar-io/indexes is what subscribers poll, with HTTPSig signing @path). Core and envoy both answer 206 correctly, so the default stack is fine and this only bites once an operator adds a cache or CDN. It fails silently, since a 200 with more bytes than asked for looks like success, and it breaks BitTorrent WebSeeds, media seeking and clients that query the Parquet bands in place over HTTP. Check every server block that serves the gateway: a TLS listener and an internal cache listener are often in different files, and fixing one leaves the other broken. After systemctl reload nginx, old workers finish their requests on the old config, so re-test a few seconds later before concluding a fix failed. The following 402 is metering, not misconfiguration: the accidental whole-file transfer consumed the address's allowance.
| Symptom | First check |
|---|---|
| 5xx from gateway | docker logs --tail 50 <core-container> (filtered), then envoy logs |
| Indexer falling behind | SQLite MAX(height) per block_importer log, peer health, /data free space |
| ClickHouse stale | docker compose --profile clickhouse logs --tail 50 clickhouse-auto-import; verify curl-to-core works from inside the container |
| Streaming table stuck at 0 | image SHA — almost always the published :latest rather than local build |
| Admin API 401/404 | ADMIN_API_KEY mismatch with .env |
| Webhook silent | WEBHOOK_TARGET_SERVERS in gateway .env; sidecars must be on the same docker network and reachable by service name |
/data filling | du -h -d 1 /data | sort -hr — usually the contiguous cache |
Observer not submitting save_observations | docker logs ar-io-node-observer-1 --since 10m | grep -iE 'crank|epoch'. Combine with ar.io get-current-epoch ... to distinguish gateway-side issues from a paused/uninitialized network |
| ArNS resolution returning 404 for names that demonstrably exist | Check the SDK pin in the container: docker exec ar-io-node-core-1 /nodejs/bin/node -e "console.log(require('@ar.io/sdk/package.json').version)". Drift behind the deployed program causes silent partial cache hydration |
/ar-io/info shows wrong programIds after migration | .env likely has explicit *_IMAGE_TAG lines pinning the old images. Update them or remove to let compose defaults take over |
Quick-scan list of the prometheus metrics that surface most operator-grade issues. All exposed at /ar-io/__gateway_metrics on core's HTTP port.
| Metric | Surfaces |
|---|---|
bundles_unbundled_total (rate) | bundle-level throughput |
data_items_indexed_total{parent_type="transaction"} (rate) | data-item write rate (SQLite writer capacity) |
bundles_unbundle_in_flight | parser workers currently active — if pinned at worker count for minutes, suspect cascade hangs |
queue_length{queue_name="…"} | the 4-queue chain — see "ANS-104 unbundling pipeline" |
data_items_dropped_total{queue_name="…"} | silent data loss at indexer hard cap. Any nonzero rate is a config issue, not a transient |
bundles_unbundle_skipped_total{reason="…"} | high_queue_depth (soft gate engaged), queue_full (BRW pushing past cap), no_workers (config error) |
| Metric | Surfaces |
|---|---|
get_data_stream_successes_total{class,source} | which cascade tier / which peer actually serves bytes |
get_data_stream_errors_total{class,source} | tier/peer failure rates. Persistent 100 % errors on a peer mean it's dead but the network process still advertises it |
get_data_errors_total{…} | pre-stream errors (connection refused, 4xx, 5xx) |
| Metric | What it means |
|---|---|
nodejs_event_loop_utilization | 0–1, sampled at scrape. Near 1.0 = main thread pegged |
nodejs_eventloop_lag_p99_seconds | tail latency on the event loop. > 0.1 s under load = something blocking on the main thread |
process_resident_memory_bytes | overall memory growth; sudden steps usually map to a queue filling |
httpsig_signing_duration_seconds_* | per-response signing cost (the histogram is more honest than benchmarking) |
in_flight = N for the configured worker count with throughput still climbing is the healthy steady state; only worry if the corresponding _total counter is also flat for many minutes.release — handles version bumps, image SHA pinning, tags, GitHub releases.testing — picks the right test layer (unit / property / e2e / auto-verify / parquet integration / load test) for a given change.vilenarios-gateway) — adds the local-machine specifics (domains, paths, sidecar config) on top of this generic skill.This skill covers the operations layer: what's running, where, and how to keep it that way. For code-level guidance see CLAUDE.md in the relevant repo.
© ar-io, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file (scripts) in .claude/skills/ar-io-gateway-operator of ar-io/ar-io-node.
Open the folder on GitHubat commit aee1c67
Ar Io Gateway Operator next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Ar Io Gateway Operator this skillar-io/ar-io-node | 127 | — | ~8.8k | Automated safety check: Notes | AGPL-3.0 | |
| Apollo Clientapollographql/skills | 117 | — | ~1.9k | Automated safety check: Pass | MIT | |
| Fireflies Deploy Integrationjeremylongshore/tons-of-skills-marketplace | 2.8k | — | ~1k | Automated safety check: Pass | MIT | |
| Shopify Performance Tuningjeremylongshore/tons-of-skills-marketplace | 2.8k | — | ~1.1k | Automated safety check: Pass | MIT | |
| ClickHouse RowBinary for Node.jsClickHouse/agent-skills | 545 | — | ~1.3k | Automated safety check: Pass | Apache-2.0 | |
| Releasehypequery/hypequery | 103 | — | ~623 | Automated safety check: Pass | Custom licence |
apollographql/skills
Guide for building React applications with Apollo Client 4.x.
jeremylongshore/tons-of-skills-marketplace
Deploy a Fireflies GraphQL worker or Webhooks V2 receiver with managed secrets, raw-body verification, bounded queues, and reversible rollout.
jeremylongshore/tons-of-skills-marketplace
Optimize Shopify API performance with GraphQL query cost reduction, bulk operations, caching strategies, and Storefront API for high-traffic storefronts.
ClickHouse/agent-skills
Generates TypeScript or JavaScript readers and writers for ClickHouse's RowBinary formats over HTTP in Node.js, after checking that RowBinary suits the data.
hypequery/hypequery
Cut a stable hypequery release via Changesets, or explain/check the canary flow.
PostHog/posthog
Audit already-implemented Data warehouse import sources for endpoints, schemas, and tables the vendor's API offers but we never wired up.
ar-io/ar-io-node
Drive the AR.IO Node release process end-to-end — preflight checks, prepare commit, finalize with image SHAs, test docker compose profiles, tag & publish, and post-release cleanup.
ar-io/ar-io-node
Decision guide for testing in the ar-io-node repo — which test layer to use, how to run it, and which helpers to reach for.
Works with
Categories
Operate any AR.IO node deployment — architecture, daily ops, diagnostics, and recurring pitfalls that apply to every operator. Ar Io Gateway Operator is an agent skill from ar-io/ar-io-node.IO node deployment — architecture, daily ops, diagnostics, and recurring pitfalls that apply to every operator.
Ar Io Gateway Operator fits situations like: the user asks about gateway health; clickHouse pipeline; graphQL routing; arNS resolution.
Run `npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a claude-code`. Or copy the skill folder (.claude/skills/ar-io-gateway-operator in ar-io/ar-io-node) into .claude/skills/ar-io-gateway-operator in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a codex`. Or copy the skill folder (.claude/skills/ar-io-gateway-operator in ar-io/ar-io-node) into .agents/skills/ar-io-gateway-operator in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ar-io/ar-io-node --skill ar-io-gateway-operator -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/ar-io-gateway-operator, .gemini/skills/ar-io-gateway-operator, .github/skills/ar-io-gateway-operator and .opencode/skills/ar-io-gateway-operator in your project.
Going by SKILL.md and its folder, Ar Io Gateway Operator needs the command-line tools its instructions call (docker, curl and npm) and credentials named ADMIN_API_KEY, SOLANA_PRIVATE_KEY and OBSERVER_PRIVATE_KEY. Our summary lists: Docker; A credential in SOLANA_PRIVATE_KEY; A credential in OBSERVER_PRIVATE_KEY.
SKILL.md names 1 domain. In commands or code: turbo-gateway.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file; runs commands with sudo), nothing it rates as a warning. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Ar Io Gateway Operator is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8.8k tokens (SKILL.md is roughly 35k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Ar Io Gateway Operator: Apollo Client (apollographql/skills, 117 stars), Fireflies Deploy Integration (jeremylongshore/tons-of-skills-marketplace, 2.8k stars), Shopify Performance Tuning (jeremylongshore/tons-of-skills-marketplace, 2.8k stars) and ClickHouse RowBinary for Node.js (ClickHouse/agent-skills, 545 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
ar-io (a GitHub organization) maintains it in ar-io/ar-io-node, which has 127 GitHub stars. The repository holds 3 skills in this directory. The repository was last updated on October 10, 2026.
Source: ar-io/ar-io-node on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.