') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - hirosystems/token-metadata-api: Stacks Token Metadata API · GitHub
Skip to content

Token Metadata API

A microservice that indexes metadata for all Fungible, Non-Fungible, and Semi-Fungible Tokens on the Stacks blockchain and exposes it via JSON REST API endpoints. It connects directly to a Stacks node via the Stacks Node Publisher (SNP) Redis event stream, processing every new block to discover token contracts, track mints and burns, and fetch off-chain metadata.

Features

  • Complete SIP-016 metadata ingestion for
  • Real-time block ingestion via the Stacks Node Publisher (SNP) Redis event stream
  • Automatic metadata refreshes via SIP-019 notifications
  • Metadata localization support
  • Metadata fetching via http:, https:, data: URIs, plus customizable gateways for IPFS and Arweave
  • Live tracking of FT/SFT supply through mint and burn event deltas
  • Easy to use REST JSON endpoints with ETag caching
  • Prometheus metrics for job queue status, contract and token counts, API performance, and more
  • Optional image cache/CDN via Google Cloud Storage or Amazon S3
  • Run modes (default, readonly, writeonly) for auto-scaling deployments
  • Admin RPC server for operational tasks (retry failed jobs, refresh metadata, import contracts)

API reference

See the Token Metadata API Reference for full endpoint documentation.

Client library

A fully typed TypeScript client is available for consuming the API. Install it with:

npm install @stacks/token-metadata-api-client

See the client README or the npm package for usage examples.

Quick start

System requirements

ComponentVersion / Notes
Node.js>= 22
PostgreSQL>= 15 (local, writable)
Stacks nodeFully synchronized, with the RPC interface accessible
RedisRequired for the SNP event stream (SNP_REDIS_URL)
Cloud object storage(Optional) Google Cloud Storage or Amazon S3 for token image caching

Running the service

Clone the repo:

git clone https://github.com/hirosystems/token-metadata-api.git
cd token-metadata-api

Create an .env file and specify the appropriate values. At a minimum you need:

# Stacks nodeSTACKS_NODE_RPC_HOST=127.0.0.1STACKS_NODE_RPC_PORT=20443# SNP event stream (Redis)SNP_REDIS_URL=redis://127.0.0.1:6379SNP_REDIS_STREAM_KEY_PREFIX=stacks-node# PostgreSQLPGHOST=127.0.0.1PGPORT=5432PGUSER=postgresPGPASSWORD=postgresPGDATABASE=token_metadata

See the Configuration section below for every available option.

Build and start:

npm install
npm run build
npm run start

The API server starts on port 3000 by default, the Admin RPC server on port 3001, and Prometheus metrics on port 9153.

Run modes

The RUN_MODE environment variable controls which components the service starts. This allows you to scale the read and write paths independently:

ModeBackground servicesAPI serverUse case
defaultYes (Job queue, SNP stream, Admin RPC)YesSingle-instance deployments
readonlyNoYesHorizontally-scaled API replicas
writeonlyYes (Job queue, SNP stream, Admin RPC)NoDedicated indexing instance

In an auto-scaled cluster you would typically run onewriteonly instance that ingests data and multiplereadonly instances behind a load balancer to serve API traffic.

Stopping the service

Always prefer sending SIGINT (Ctrl+C) instead of SIGKILL. This allows the service to finish any in-progress jobs, flush writes, and cleanly disconnect from PostgreSQL and Redis.

Configuration

All configuration is done via environment variables. Defaults are shown in parentheses.

Core settings

VariableDescriptionDefault
RUN_MODEdefault, readonly, or writeonlydefault
NETWORKmainnet or testnetmainnet
API_HOSTAPI server bind address0.0.0.0
API_PORTAPI server port3000
ADMIN_RPC_PORTAdmin RPC server port3001
PROMETHEUS_PORTPrometheus metrics port9153

Stacks node & SNP

VariableDescriptionDefault
STACKS_NODE_RPC_HOSTStacks node RPC hostname(required)
STACKS_NODE_RPC_PORTStacks node RPC port(required)
STACKS_API_BASE_URLStacks API base URL (for admin contract imports)https://api.mainnet.hiro.so
SNP_REDIS_URLRedis URL for the SNP event stream(required)
SNP_REDIS_STREAM_KEY_PREFIXRedis stream key prefix(required)

PostgreSQL

VariableDescriptionDefault
PGHOSTDatabase host(required)
PGPORTDatabase port5432
PGUSERDatabase user(required)
PGPASSWORDDatabase password(required)
PGDATABASEDatabase name(required)
PG_CONNECTION_POOL_MAXMax connections in pool10
PG_IDLE_TIMEOUTIdle connection timeout (seconds)30
PG_MAX_LIFETIMEMax connection lifetime (seconds)60
PG_CLOSE_TIMEOUTConnection close timeout (seconds)10

Job queue

VariableDescriptionDefault
JOB_QUEUE_AUTO_STARTAutomatically start the queue on boottrue
JOB_QUEUE_STRICT_MODEEnable strict processing modefalse
JOB_QUEUE_SIZE_LIMITNumber of pending jobs loaded into memory per batch200
JOB_QUEUE_CONCURRENCY_LIMITNumber of jobs executed simultaneously5
JOB_QUEUE_MAX_RETRIESMax retry attempts for a failed job10
JOB_QUEUE_TIMEOUT_MSTimeout per job (ms)60000
JOB_QUEUE_RETRY_AFTER_MSDelay before retrying a failed job (ms)5000
JOB_QUEUE_INVALID_RETRY_AFTER_MSDelay before re-processing a job marked invalid (ms). Tokens are re-enqueued on every re-mint, so this keeps a contract with unparseable metadata from being re-fetched on each mint3600000

Metadata fetching

VariableDescriptionDefault
METADATA_FETCH_TIMEOUT_MSHTTP timeout for fetching metadata (ms)30000
METADATA_MAX_IMMEDIATE_URI_RETRIESMax immediate retries for a metadata URI3
METADATA_MAX_PAYLOAD_BYTE_SIZEMax metadata JSON payload size (bytes)1000000
METADATA_MAX_NFT_CONTRACT_TOKEN_COUNTMax tokens to index per NFT contract50000
METADATA_DYNAMIC_TOKEN_REFRESH_INTERVALInterval for dynamic token refreshes (seconds)86400
METADATA_RATE_LIMITED_HOST_RETRY_AFTERWait time after a 429 response (seconds)60
METADATA_FETCH_MAX_REDIRECTIONSMax HTTP redirects to follow5

IPFS & Arweave gateways

VariableDescriptionDefault
PUBLIC_GATEWAY_IPFSIPFS gateway URLhttps://cloudflare-ipfs.com
PUBLIC_GATEWAY_IPFS_EXTRA_HEADERExtra header for IPFS gateway requests(none)
PUBLIC_GATEWAY_IPFS_REPLACEDComma-separated list of IPFS gateways to replace(common gateways)
PUBLIC_GATEWAY_ARWEAVEArweave gateway URLhttps://arweave.net

Image cache

VariableDescriptionDefault
IMAGE_CACHE_PROCESSOR_ENABLEDEnable image cachingfalse
IMAGE_CACHE_UPLOAD_PROVIDERImage upload backend (gcs or aws)gcs
IMAGE_CACHE_RESIZE_WIDTHThumbnail width (px)300
IMAGE_CACHE_GCS_BUCKET_NAMEGoogle Cloud Storage bucket name(none)
IMAGE_CACHE_GCS_OBJECT_NAME_PREFIXObject name prefix in GCS(none)
IMAGE_CACHE_AWS_BUCKET_NAMEAmazon S3 bucket name(none)
IMAGE_CACHE_AWS_REGIONAWS region for S3 uploads(none)
IMAGE_CACHE_AWS_OBJECT_NAME_PREFIXObject name prefix in S3(none)
IMAGE_CACHE_CDN_BASE_PATHCDN base URL for cached images(none)
IMAGE_CACHE_MAX_BYTE_SIZEMax image size to cache (bytes)(none)

Bugs and feature requests

If you encounter a bug or have a feature request, we encourage you to follow the steps below:

  1. Search for existing issues: Before submitting a new issue, please search existing and closed issues to check if a similar problem or feature request has already been reported.
  2. Open a new issue: If it hasn't been addressed, please open a new issue. Choose the appropriate issue template and provide as much detail as possible, including steps to reproduce the bug or a clear description of the requested feature.
  3. Evaluation SLA: Our team reads and evaluates all the issues and pull requests. We are available Monday to Friday and we make a best effort to respond within 7 business days.

Please do not use the issue tracker for personal support requests or to ask for the status of a transaction. You'll find help at the #support Discord channel.

Contribute

Development of this product happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving the product.

Code of Conduct

Please read our Code of conduct since we expect project participants to adhere to it.

Contributing Guide

Read our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes.

Community

Join our community and stay connected with the latest updates and discussions:

About

Stacks Token Metadata API

Resources

Code of conduct

Contributing

Security policy

Stars

10 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages