# Aradia Data API: complete agent context Source: https://docs.aradia.app/guides/authentication.md # Authentication How to authenticate with the Aradia Data API Every request carries a bearer API key in the `Authorization` header. ``` Authorization: Bearer $ARADIA_API_TOKEN ``` ## Trying an endpoint without a key Every page in the API Reference carries a console for the endpoint it documents, and each one issues itself a read-only demo token, so you can call the API before signing up for anything. The token is filled in for you and renewed when it expires. Demo tokens are read-only, expire 15 minutes after they are issued, and are limited to 1 request per second. Issuance itself is capped per IP address. They exist to answer "what does this endpoint return", not to run against. You can ask for one directly: see [`POST /v1/demo-token`](/reference/demo-token/). Only its hash is stored, so a token cannot be recovered afterwards. Request another instead. ## Getting a key Sign in to the [Developer Portal](https://portal.aradia.app/) with your wallet and create a key. The secret is shown exactly once; store it in your secret manager. Paste your key into any console and it stops managing the token for you. Clear the field to hand control back. :::caution Never embed a key in a browser bundle, a public repository or a URL query string. Keys belong in server-side environment variables. ::: ## Scopes Keys are issued with read scopes only: | Scope | Grants | | --- | --- | | `read:collections` | Collection listings and details | | `read:nfts` | Token metadata and collection tokens | | `read:owners` | Tokens held by an address | | `read:transfers` | Transfer history | | `read:activity` | Global activity feed | | `read:status` | Indexer status and coverage | ## Rotation and revocation Rotate a key to obtain a new secret, or revoke it to reject it immediately. Both actions live in the portal and are recorded in an audit trail. Source: https://docs.aradia.app/guides/errors.md # Errors Error codes and retry guidance Errors share one envelope and never leak internal details, SQL, or stack traces. ```json { "error": { "code": "NOT_FOUND", "message": "Collection not found" } } ``` | Status | Code | Meaning | | --- | --- | --- | | `400` | `INVALID_REQUEST` | Malformed parameter, cursor or filter | | `401` | `UNAUTHORIZED` | Missing, malformed, expired or revoked key | | `403` | `FORBIDDEN` | Key lacks the required scope, or origin not allowed | | `404` | `NOT_FOUND` | No such collection, token or owner | | `429` | `RATE_LIMITED` | Rate limit exceeded | | `503` | `UNAVAILABLE` | Dependency unavailable; retry with backoff | ## Retry guidance - Retry `429` and `503` with exponential backoff and jitter. - Never retry `400`, `401`, `403` or `404` unchanged. Source: https://docs.aradia.app/guides/introduction.md # Introduction Getting started with the Aradia Data API The **Aradia Data API** provides read-only access to indexed NFT data on Astar Network: collections, tokens, owners, transfers and activity. ## Quick start 1. Create a read-only API key in the [Developer Portal](https://portal.aradia.app/). 2. Export it locally: `export ARADIA_API_TOKEN=your_key` 3. Call an endpoint: ```bash curl -sS \ -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/indexer/status" ``` ## Conventions - All timestamps are UTC, RFC 3339. - Addresses are lowercase hex with a `0x` prefix. - Lists use [cursor pagination](/guides/pagination), never numeric skips. - Wei amounts are strings so no precision is lost. - [Authentication](https://docs.aradia.app/guides/authentication): How to authenticate requests. - [Pagination](https://docs.aradia.app/guides/pagination): Cursor-based paging. - [Rate Limits](https://docs.aradia.app/guides/rate-limits): Free profile limits. - [Errors](https://docs.aradia.app/guides/errors): Error codes and retry guidance. Source: https://docs.aradia.app/guides/pagination.md # Pagination How cursor pagination works in the Aradia Data API List endpoints use opaque cursors. Cursors encode the exact position in a stable sort, so results stay consistent while the indexer keeps writing new blocks. ## Requesting a page ```bash curl -sS \ -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/activity?limit=100" ``` ## Response envelope ```json { "data": [ ... ], "pagination": { "limit": 100, "next_cursor": "b3BhcXVl", "has_more": true } } ``` ## Rules - Treat cursors as opaque; never build or mutate one. - Stop when `has_more` is `false`. - A cursor from one endpoint is not valid on another. - There is no numeric skip parameter, by design. Source: https://docs.aradia.app/guides/rate-limits.md # Rate Limits Request limits for the Aradia Data API The first release ships a single free profile. There is no paid tier and no billing. | Limit | Free profile | | --- | --- | | Requests per second | 5 | | Requests per minute | 300 | | Concurrent requests | 10 | ## When you exceed a limit The API replies `429`. Back off, then retry. Limits are enforced per key and per client IP. ```json { "error": { "code": "RATE_LIMITED", "message": "Too many requests" } } ``` :::tip During a limiter outage the API fails closed with `503` rather than serving unbounded traffic. Retry with exponential backoff. ::: Source: https://docs.aradia.app/use-cases/ai-agent-integration.md # Vibe code an NFT app with an AI coding agent Give an AI coding agent Aradia's llms.txt, OpenAPI contract, demo token, and reliable NFT media rules for Astar development. An agent writes a better integration when it receives the machine-readable contract, the task guide, and the API's data rules. Give it these sources before asking it to generate code: - [`llms.txt`](https://docs.aradia.app/llms.txt) for the documentation index. - [`llms-full.txt`](https://docs.aradia.app/llms-full.txt) for the complete agent-readable guide set. - [`openapi.yaml`](https://docs.aradia.app/openapi.yaml) for routes, schemas, scopes, and examples. - The use-case page closest to the product you want to build. ## Fetch the agent context ### cURL ```bash curl -sS https://docs.aradia.app/llms.txt curl -sS https://docs.aradia.app/openapi.yaml ``` ### JavaScript ```javascript const [index, contract] = await Promise.all([ fetch("https://docs.aradia.app/llms.txt").then((response) => response.text()), fetch("https://docs.aradia.app/openapi.yaml").then((response) => response.text()), ]); const context = `${index}\n\n${contract}`; ``` ### Python ```python import requests index = requests.get("https://docs.aradia.app/llms.txt", timeout=30).text contract = requests.get("https://docs.aradia.app/openapi.yaml", timeout=30).text context = f"{index}\n\n{contract}" ``` ## Prompt with the rules that matter ```text Build an Astar NFT wallet gallery using the Aradia Data API. Use the attached OpenAPI contract as the source of truth. Keep token IDs and blockchain counters as strings. Follow pagination.next_page_params until has_more is false. Render nft.media_url when present, otherwise fall back to nft.image_url. Read the API token from a server-side ARADIA_API_TOKEN environment variable. Do not put the token in browser code, URLs, logs, or committed files. Handle 429 and 503 with bounded retry and backoff. ``` ## Let the agent test without your key The reference console obtains a read-only demo token automatically. An agent can also call `POST /v1/demo-token`, use the returned bearer token for 15 minutes, and replace it with a portal key only when the integration is ready for application traffic. Treat generated code as a first draft. Verify the requested scopes, pagination loop, error handling, and whether it keeps secrets on the server before deploying it. Source: https://docs.aradia.app/use-cases/collection-gallery.md # Build an Astar NFT collection gallery Page through an Astar NFT collection, preserve token ID precision, and prefer locally stored NFT media. Combine the collection detail with its token pages. The detail supplies contract context and counts; the token endpoint supplies ownership and artwork. ### cURL ```bash COLLECTION=0x0000000000000000000000000000000000000001 curl -sS -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/collections/$COLLECTION" curl -sS -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/collections/$COLLECTION/nfts?limit=100" ``` ### JavaScript ```javascript const base = "https://api.aradia.app/v1"; const headers = { Authorization: `Bearer ${process.env.ARADIA_API_TOKEN}` }; const collection = "0x0000000000000000000000000000000000000001"; const [detailResponse, tokensResponse] = await Promise.all([ fetch(`${base}/collections/${collection}`, { headers }), fetch(`${base}/collections/${collection}/nfts?limit=100`, { headers }), ]); if (!detailResponse.ok || !tokensResponse.ok) throw new Error("Collection request failed"); const [{ data: detail }, tokens] = await Promise.all([ detailResponse.json(), tokensResponse.json(), ]); const items = tokens.data.map((nft) => ({ ...nft, src: nft.media_url ?? nft.image_url })); ``` ### Python ```python import os import requests base = "https://api.aradia.app/v1" collection = "0x0000000000000000000000000000000000000001" headers = {"Authorization": f"Bearer {os.environ['ARADIA_API_TOKEN']}"} detail = requests.get(f"{base}/collections/{collection}", headers=headers, timeout=30) tokens = requests.get( f"{base}/collections/{collection}/nfts", params={"limit": 100}, headers=headers, timeout=30, ) detail.raise_for_status() tokens.raise_for_status() gallery = [{**nft, "src": nft.get("media_url") or nft.get("image_url")} for nft in tokens.json()["data"]] ``` ## Pagination rule Do not increment a page number. Copy `after_token_id` and `after_id` from `next_page_params`. Token IDs must remain strings from request to render, even when they look numeric. Related reference: [`GET /v1/collections/{address}`](/reference/collections/#get-v1-collections-address) and [`GET /v1/collections/{address}/nfts`](/reference/nfts/#get-v1-collections-address-nfts). Source: https://docs.aradia.app/use-cases/enriched-profile-feed.md # Build an enriched NFT profile feed Combine wallet holdings, collection context, transfer history, and reliable media into an Astar NFT profile. An enriched profile is a composition, not a single endpoint: 1. Load current holdings with `GET /v1/owners/{address}/nfts`. 2. Deduplicate `collection_address`, then fetch each collection once. 3. Fetch transfer history only for the visible tokens that need a timeline. 4. Prefer `media_url`, then fall back to `image_url`. ### cURL ```bash OWNER=0x0000000000000000000000000000000000000002 curl -sS -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/owners/$OWNER/nfts?limit=100" ``` ### JavaScript ```javascript const base = "https://api.aradia.app/v1"; const headers = { Authorization: `Bearer ${process.env.ARADIA_API_TOKEN}` }; async function enrichedProfile(owner) { const holdingsResponse = await fetch(`${base}/owners/${owner}/nfts?limit=100`, { headers }); if (!holdingsResponse.ok) throw new Error(`Holdings failed: ${holdingsResponse.status}`); const holdings = await holdingsResponse.json(); const addresses = [...new Set(holdings.data.map((nft) => nft.collection_address))]; const collections = await Promise.all(addresses.map(async (address) => { const response = await fetch(`${base}/collections/${address}`, { headers }); if (!response.ok) throw new Error(`Collection failed: ${response.status}`); return (await response.json()).data; })); const byAddress = new Map(collections.map((item) => [item.contract_address, item])); const items = holdings.data.map((nft) => ({ ...nft, artwork: nft.media_url ?? nft.image_url, collection: byAddress.get(nft.collection_address), })); return { items, coverage: holdings.coverage }; } ``` ### Python ```python import os import requests base = "https://api.aradia.app/v1" headers = {"Authorization": f"Bearer {os.environ['ARADIA_API_TOKEN']}"} def enriched_profile(owner): holdings = requests.get(f"{base}/owners/{owner}/nfts", params={"limit": 100}, headers=headers, timeout=30) holdings.raise_for_status() page = holdings.json() addresses = sorted({nft["collection_address"] for nft in page["data"]}) collections = {} for address in addresses: response = requests.get(f"{base}/collections/{address}", headers=headers, timeout=30) response.raise_for_status() collections[address] = response.json()["data"] return [ {**nft, "artwork": nft.get("media_url") or nft.get("image_url"), "collection": collections[nft["collection_address"]]} for nft in page["data"] ] ``` ## Keep the request count bounded Do not request collection details once per NFT. Forty NFTs from six collections require six detail requests, not forty. Fetch transfer history only when a token is visible or its timeline is opened. For a chronological profile feed, merge the selected transfer arrays by `block_timestamp` and deduplicate with `transaction_hash` plus `log_index`. Source: https://docs.aradia.app/use-cases/indexer-health.md # Check Astar NFT data freshness and coverage Use Aradia indexer status and response coverage before presenting NFT ownership or activity as complete. Freshness and completeness answer different questions. Indexer status says how recently Aradia observed the chain. Each list response also carries `coverage`, which says whether the indexed historical window is known to be complete. ### cURL ```bash curl -sS \ -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/indexer/status?network=astar" ``` ### JavaScript ```javascript async function dataConfidence(token) { const response = await fetch("https://api.aradia.app/v1/indexer/status?network=astar", { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) return { usable: false, reason: `status ${response.status}` }; const { data } = await response.json(); const observed = BigInt(data.last_seen_block); const confirmed = BigInt(data.last_confirmed_block); return { usable: true, confirmationsBehindHead: observed - confirmed, coverage: data.coverage_status, checkedAt: data.updated_at, }; } ``` ### Python ```python import requests def data_confidence(token): response = requests.get( "https://api.aradia.app/v1/indexer/status", params={"network": "astar"}, headers={"Authorization": f"Bearer {token}"}, timeout=30, ) response.raise_for_status() status = response.json()["data"] return { "confirmations_behind_head": int(status["last_seen_block"]) - int(status["last_confirmed_block"]), "coverage": status["coverage_status"], "checked_at": status["updated_at"], } ``` ## Product language for uncertain coverage - `complete`: the API claims the documented window is covered. - `partial`: show the result, but label it as incomplete. - `unknown`: avoid claims such as "all NFTs" or "complete history". Large block values are decimal strings. Parse them with `BigInt` in JavaScript, not `Number`. Source: https://docs.aradia.app/use-cases/live-activity-feed.md # Build a live Astar NFT activity feed Poll canonical NFT mint, transfer, and burn activity on Astar without duplicating events in your interface. `GET /v1/activity` returns the newest canonical events first. Filter by collection or event type when the product has a narrower purpose. ### cURL ```bash curl -sS \ -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/activity?limit=50&event_type=transfer" ``` ### JavaScript ```javascript const seen = new Set(); async function pollActivity(token) { const response = await fetch("https://api.aradia.app/v1/activity?limit=50", { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) throw new Error(`Activity failed: ${response.status}`); const page = await response.json(); return page.data.filter((event) => { const identity = `${event.transaction_hash}:${event.log_index}`; if (seen.has(identity)) return false; seen.add(identity); return true; }); } ``` ### Python ```python import requests seen = set() def poll_activity(token): response = requests.get( "https://api.aradia.app/v1/activity", params={"limit": 50}, headers={"Authorization": f"Bearer {token}"}, timeout=30, ) response.raise_for_status() fresh = [] for event in response.json()["data"]: identity = (event["transaction_hash"], event["log_index"]) if identity not in seen: seen.add(identity) fresh.append(event) return fresh ``` ## Poll without wasting quota Start with a 12-second interval, close to Astar's block time, and pause when the page is hidden. Keep a bounded set of recent `transaction_hash` plus `log_index` identities so every event is rendered once. For older history, follow `before_timestamp` and `before_id`. For new events, request the first page again and deduplicate locally. Source: https://docs.aradia.app/use-cases/nft-provenance.md # Build an NFT provenance timeline on Astar Combine current NFT metadata with canonical transfer history to explain where an Astar NFT came from. Fetch the token and its transfer history together. The token response gives the current owner and artwork; transfers provide the ordered mint, transfer, and burn events. ### cURL ```bash COLLECTION=0x0000000000000000000000000000000000000001 TOKEN_ID=1 curl -sS -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/nfts/$COLLECTION/$TOKEN_ID" curl -sS -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/nfts/$COLLECTION/$TOKEN_ID/transfers?limit=100" ``` ### JavaScript ```javascript async function provenance(collection, tokenId, token) { const base = `https://api.aradia.app/v1/nfts/${collection}/${tokenId}`; const headers = { Authorization: `Bearer ${token}` }; const [nftResponse, transfersResponse] = await Promise.all([ fetch(base, { headers }), fetch(`${base}/transfers?limit=100`, { headers }), ]); if (!nftResponse.ok || !transfersResponse.ok) throw new Error("Provenance request failed"); const nft = (await nftResponse.json()).data; const transfers = (await transfersResponse.json()).data; return { nft: { ...nft, artwork: nft.media_url ?? nft.image_url }, timeline: transfers.slice().reverse(), }; } ``` ### Python ```python import requests def provenance(collection, token_id, token): base = f"https://api.aradia.app/v1/nfts/{collection}/{token_id}" headers = {"Authorization": f"Bearer {token}"} nft = requests.get(base, headers=headers, timeout=30) transfers = requests.get(f"{base}/transfers", params={"limit": 100}, headers=headers, timeout=30) nft.raise_for_status() transfers.raise_for_status() token_data = nft.json()["data"] token_data["artwork"] = token_data.get("media_url") or token_data.get("image_url") return {"nft": token_data, "timeline": list(reversed(transfers.json()["data"]))} ``` ## Explain confirmation depth honestly `confirmations` is the number of blocks mined on top of an event. It can be `null` for historical rows whose block number is unknown. Do not turn `null` into zero, because unknown depth is not the same as an unconfirmed transfer. The endpoint excludes rows explicitly invalidated by a chain reorganisation. Apply the confirmation threshold appropriate for your product before calling an event settled. Source: https://docs.aradia.app/use-cases/token-gating.md # Add NFT token gating on Astar Check whether an Astar wallet owns an NFT from a required collection with the Aradia Data API. Token gating answers one question: does this wallet currently hold at least one live token from the required collection? The owner endpoint is authoritative for current holdings, but it does not accept a collection filter, so a wallet with more than 100 NFTs must be paged until a match is found or the list ends. ### cURL ```bash curl -sS \ -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/owners/0x0000000000000000000000000000000000000002/nfts?limit=100" ``` ### JavaScript ```javascript async function ownsCollection(owner, requiredCollection, token) { const base = `https://api.aradia.app/v1/owners/${owner}/nfts`; let params = new URLSearchParams({ limit: "100" }); while (true) { const response = await fetch(`${base}?${params}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!response.ok) throw new Error(`Aradia API error ${response.status}`); const page = await response.json(); if (page.data.some((nft) => nft.collection_address === requiredCollection && !nft.is_burned)) { return true; } if (!page.pagination.has_more) return false; params = new URLSearchParams({ limit: "100" }); for (const [key, value] of Object.entries(page.pagination.next_page_params)) { params.set(key, String(value)); } } } ``` ### Python ```python import requests def owns_collection(owner, required_collection, token): url = f"https://api.aradia.app/v1/owners/{owner}/nfts" params = {"limit": 100} while True: response = requests.get(url, params=params, headers={"Authorization": f"Bearer {token}"}, timeout=30) response.raise_for_status() page = response.json() if any(nft["collection_address"] == required_collection and not nft["is_burned"] for nft in page["data"]): return True if not page["pagination"]["has_more"]: return False params = {"limit": 100, **page["pagination"]["next_page_params"]} ``` ## Enforce access on the server Never trust a boolean sent by the browser. Run the ownership check in your backend, associate the checked wallet with a verified wallet session, and cache a positive result only for a short period if immediate transfer revocation matters. Check the response `coverage.status` before presenting the result as exhaustive. `unknown` means the API is not claiming complete historical coverage. Source: https://docs.aradia.app/use-cases/wallet-nft-gallery.md # Build a wallet NFT gallery on Astar Fetch every NFT owned by an Astar wallet and render reliable artwork with Aradia's local media fallback. Use `GET /v1/owners/{address}/nfts` to load current holdings. Token IDs stay as strings, and `media_url` is included only when Aradia already stores the original bytes. ## Request the first page ### cURL ```bash curl -sS \ -H "Authorization: Bearer $ARADIA_API_TOKEN" \ "https://api.aradia.app/v1/owners/0x0000000000000000000000000000000000000002/nfts?limit=100" ``` ### JavaScript ```javascript const api = "https://api.aradia.app"; const owner = "0x0000000000000000000000000000000000000002"; const response = await fetch(`${api}/v1/owners/${owner}/nfts?limit=100`, { headers: { Authorization: `Bearer ${process.env.ARADIA_API_TOKEN}` }, }); if (!response.ok) throw new Error(`Aradia API error ${response.status}`); const page = await response.json(); const gallery = page.data.map((nft) => ({ id: `${nft.collection_address}:${nft.token_id}`, title: nft.name ?? `Token ${nft.token_id}`, src: nft.media_url ?? nft.image_url ?? null, mediaType: nft.media_type ?? "image", })); ``` ### Python ```python import os import requests owner = "0x0000000000000000000000000000000000000002" response = requests.get( f"https://api.aradia.app/v1/owners/{owner}/nfts", params={"limit": 100}, headers={"Authorization": f"Bearer {os.environ['ARADIA_API_TOKEN']}"}, timeout=30, ) response.raise_for_status() gallery = [ { "id": f"{nft['collection_address']}:{nft['token_id']}", "src": nft.get("media_url") or nft.get("image_url"), } for nft in response.json()["data"] ] ``` ## Render media without broken promises Use `media_url ?? image_url`. A present `media_url` means the original is already on Aradia's disk. When it is absent, `image_url` is the metadata source and may still depend on IPFS availability. Use `media_type` to choose the element: ```javascript function NFTMedia({ nft }) { const src = nft.media_url ?? nft.image_url; if (!src) return
; if (nft.media_type?.startsWith("video/")) return ; return