Pentagon NFT Data API

Cross-chain NFT ownership, metadata, and transfer history for the Pentagon Games ecosystem.

Live 49 Collections 14 Chains

🚀 Getting Started

Get up and running with the NFT Data API in three steps.

Step 1: Get an App Key

Register at cli.pentagon.games or contact the Pentagon Games team:

Step 2: Add the Header

Include your App Key in all requests for higher rate limits:

X-PG-App-Key: pk_live_your_key

Step 3: Query NFTs

Look up ownership, metadata, and transfers across 14 chains with a single API call.

Public access available: All endpoints work without an app key at a reduced rate limit (10 req/min). Authenticated requests get up to 100-500 req/min depending on your key tier.

Overview

The Pentagon NFT Data API provides unified NFT ownership tracking across the Pentagon Games ecosystem. Query any wallet, any chain, any collection — one API, one answer.

🔍 Ownership Lookup

Find all NFTs owned by any wallet address, filterable by chain, collection, or contract.

🌐 Multi-Chain

Ethereum, Polygon, BSC, Arbitrum, Pentagon Chain, and 9 more networks tracked in real-time.

📊 Metadata & Stats

Token metadata, traits, collection stats, holder counts, and transfer history.

⚡ Fast & Reliable

Sub-second queries backed by a four-layer sync engine. Pentagon chain gets real-time updates.

Base URL

https://nft-data.pentagon.games

Authentication

Pass your app key via the X-PG-App-Key header:

curl -H "X-PG-App-Key: pk_live_your_key" \
  "https://nft-data.pentagon.games/api/v1/nft/owner/0x1234..."

Rate Limits

Auth LevelLimitScope
No key (public)10 req/minPer IP address
App key (web)60–100 req/minPer key
App key (server)100–500 req/minPer key

Rate limits are configurable per app key. When exceeded, the API returns 429 Too Many Requests with a retry_after field.

Same keys, multiple APIs: App keys issued for the Pentagon Identity API also work for the NFT Data API. One key, multiple services.

API Endpoints

Quick Reference

MethodEndpointDescription
GET / Health check — returns service status
GET /api/v1/nft/owner/:address All NFTs owned by a wallet
GET /api/v1/nft/:contract/:tokenId Single NFT details + metadata
GET /api/v1/collections List all tracked collections
GET /api/v1/collection/{contract}/items Full item index of a collection (paginated)
GET /api/v1/collection/:contract/stats Collection ownership statistics
GET /api/v1/nft/:contract/:tokenId/transfers Transfer history for an NFT
GET /api/v1/nft/owner/:address/chains Lightweight per-chain NFT count summary
GET POST /api/v1/nft/owners Multi-address merged NFT lookup (max 10)
POST GET /api/v1/report Submit / list user "flag this" data-quality reports

Look Up NFTs by Owner

GET /api/v1/nft/owner/:wallet_address

Returns all NFTs owned by a wallet address across all tracked chains and collections.

ParameterTypeRequiredDescription
wallet_addresspathYesEthereum-style wallet address (0x...)
chain_idqueryNoFilter by chain ID (e.g., 3344 for Pentagon)
contractqueryNoFilter by contract address
collectionqueryNoFilter by collection name
group_byqueryNoSet to chain to group results by chain ID
pagequeryNoPage number (default: 1)
limitqueryNoResults per page (default: 50, max: 200)

Example request:

curl "https://nft-data.pentagon.games/api/v1/nft/owner/0x37224cFD71347Da6f097c94B8649f9C552f803c9?chain_id=3344"

Response:

{
  "owner": "0x37224cFD71347Da6f097c94B8649f9C552f803c9",
  "total": 17,
  "page": 1,
  "limit": 50,
  "nfts": [
    {
      "tokenId": "4201",
      "name": "BCSH #4201",
      "owner": "0x37224cfd71347da6f097c94b8649f9c552f803c9",
      "collection": "BCSH OASYS",
      "contractAddress": "0xcDAD57bFc48E8373280C6dc3039C5169353B6879",
      "chain": 3344,
      "standard": "ERC721",
      "image": "https://images.pfpvault.com/3344/0xcdad.../4201.webp",
      "fileType": "image",
      "mediaType": "image",
      "desc": "After shattering her katana...",
      "transferable": true,
      "updatedAt": "2026-05-11T04:15:39+00:00"
    }
  ]
}
Owner-list item fields: each nfts[] entry now carries the fields a wallet needs to render and gate actions without a second call per token: standard (ERC721 / ERC1155 — gate transfer/listing UI), fileType (alias mediaType) — a normalized media type, currently always image for any token with a renderable image (see note below), transferable (true = freely tradable, false = soulbound/locked; always present, defaults to false), and desc (short description). For the full attribute list, license tier, and storage info, call the single-token endpoint below.
fileType / mediaType values: the API returns a normalized value so clients have one rule. Today the only value returned is image (for any token that has a renderable image), or null in the rare case a token has no image at all. The underlying sync stores an unnormalized none for tokens it didn't explicitly classify (e.g. some PEGNAMES SVGs, Gemry, EtherFantasy stills) — the API collapses that to image because the image URL still renders as a still. Treatment: always render the image URL; there is no current case where image should not be shown.
Animated & 3D media (roadmap): a dedicated animation_url field for video / glTF / 3D assets (e.g. Gemry animations) is on the sync roadmap and will appear on both the owner-list and single-token endpoints once the ingestion pipeline stores it (it has no column in the DB today). When that lands, fileType/mediaType will start returning values like video or gltf. Until then, treat a missing animation_url as "still image only."

With ?group_by=chain:

{
  "chains": [
    {"chainId": 1, "count": 610, "nfts": [...]},
    {"chainId": 3344, "count": 1, "nfts": [...]}
  ]
}

Look Up NFT by Token ID

GET /api/v1/nft/:contract_address/:token_id

Returns details for a specific NFT including current owner, metadata, and trait attributes.

ParameterTypeRequiredDescription
contract_addresspathYesNFT contract address
token_idpathYesToken ID

Response:

{
  "contract": "0xcDAD57bFc48E8373280C6dc3039C5169353B6879",
  "tokenId": "5555000000553",
  "collection": "BCSH OASYS",
  "chain": 3344,
  "owner": "0x6e28399aa9dd598caa49df9fa46d0ae87c0e7d57",
  "transferable": true,
  "standard": "ERC721",
  "metadata": {
    "name": "Dark Setsuko",
    "description": "After shattering her katana...",
    "image": "https://images.pfpvault.com/3344/0xcdad.../5555000000553.webp",
    "imageSource": "cached",
    "attributes": [
      {"trait_type": "Background", "value": "Blue"}
    ]
  },
  "license": {
    "schema": "erc721l-metadata-v1",
    "rights_model": "base-plus-upgrade",
    "current_tier": 4,
    "current_name": "COMMERCIAL_DERIVS_NONEXCLUSIVE",
    "base_tier": 2,
    "upgraded_tier": 4,
    "terms_uri": "https://nft-data.pentagon.games/licenses/tier-4",
    "source": "override"
  },
  "storage": {
    "image": {
      "tier": "cached",
      "provider": "s3+cloudfront",
      "permanence": "managed",
      "url": "https://images.pfpvault.com/3344/0xcdad.../5555000000553.webp",
      "archivalOriginal": "s3://pg-nft-images/originals/3344/0xcdad.../5555000000553.png"
    },
    "metadata": {
      "tier": "override",
      "provider": "pg_nft_db",
      "tokenUri": "https://api.bcsh.xyz/metadata/5555000000553"
    }
  },
  "source": {
    "tokenUri": "https://api.bcsh.xyz/metadata/5555000000553",
    "lastSync": "2026-05-08T18:19:02+00:00"
  }
}
Top-level fields: transferable (SBT flag — false means soulbound/locked), standard (ERC721 or ERC1155), license (ERC-721L rights tier, see Metadata & Licensing), and storage (where the image and metadata physically live and how permanent they are). These are promoted to the top level for integrators; transferable and licensing values also remain mirrored inside metadata.attributes for backward compatibility.
Image resolution: The image field returns the best available source — prefers per-token CDN (images.pfpvault.com) over on-chain metadata (arweave.net). The imageSource field tells you which tier was used: "cached" (pfpvault CDN), "onchain" (from tokenURI), or "original" (fallback). This ensures upgraded NFTs (e.g. Dark Setsuko, Obelith variants) display their correct artwork.
Two-layer metadata: The metadata field contains the override we serve (fast, normalized). The source.tokenUri points to the underlying on-chain / project metadata endpoint if you need a live fetch. See Metadata & Licensing for how the override layer works.

Chain Summary for Owner

GET /api/v1/nft/owner/:address/chains

Returns a lightweight breakdown of how many NFTs a wallet holds on each chain, without fetching full NFT data. Useful for building chain selectors or quick portfolio overviews.

ParameterTypeRequiredDescription
addresspathYesEthereum-style wallet address (0x...)

Example request:

curl "https://nft-data.pentagon.games/api/v1/nft/owner/0x37224cFD71347Da6f097c94B8649f9C552f803c9/chains"

Response:

{
  "total": 611,
  "chains": [
    {"chainId": 1, "count": 610},
    {"chainId": 3344, "count": 1}
  ]
}
Use case: Call /chains first to discover which chains a user has NFTs on, then fetch per-chain with ?chain_id=3344 on the owner endpoint. This avoids loading all NFTs across all chains upfront.

Multi-Address Lookup

GET /api/v1/nft/owners?addresses=0xABC,0xDEF
POST /api/v1/nft/owners

Merges NFTs from multiple wallet addresses into a single response (max 10 addresses). Supports all the same filters as the single-address endpoint (chain_id, collection, contract, group_by).

ParameterTypeRequiredDescription
addressesquery (GET) or body (POST)YesComma-separated addresses (GET) or JSON array (POST). Max 10.
chain_idqueryNoFilter by chain ID
collectionqueryNoFilter by collection name
contractqueryNoFilter by contract address
group_byqueryNoSet to chain for per-chain grouping

GET example:

curl "https://nft-data.pentagon.games/api/v1/nft/owners?addresses=0xABC...,0xDEF...&chain_id=3344"

POST example:

curl -X POST "https://nft-data.pentagon.games/api/v1/nft/owners" \
  -H "Content-Type: application/json" \
  -H "X-PG-App-Key: pk_live_your_key" \
  -d '{"addresses": ["0xABC...", "0xDEF..."]}'
Migration from old API: If you previously used api.account.pentagon.games/user/nfts which auto-resolved linked wallets via user auth, you can now get the same result by fetching the user's linked addresses from the Pentagon Identity API, then passing them all to /api/v1/nft/owners. Same response format as the single-address endpoint.

List Tracked Collections

GET /api/v1/collections

Returns the tracked NFT collections with contract address, chain, token standard, indexed token count, and a display logo. Useful for building collection-filter UIs.

ParameterTypeRequiredDescription
chainqueryNoFilter by chain ID (alias chain_id), e.g. 3344 for Pentagon
activequeryNotrue (default), false, or all

Example request:

curl "https://nft-data.pentagon.games/api/v1/collections?chain=1"

Response:

{
  "count": 82,
  "collections": [
    {
      "name": "MoonCats",
      "logo": "https://images.pfpvault.com/1/0xc3f7.../.webp",
      "logoIsFallback": true,
      "logoFilename": null,
      "contractAddress": "0xc3f733ca98e0dad0386979eb96fb1722a1a05e69",
      "chain": 1,
      "standard": "ERC721",
      "count": 20921
    },
    {
      "name": "Gunnies",
      "logo": "https://pentagon.games/images/gunnies-logo.png",
      "logoIsFallback": false,
      "logoFilename": "https://pentagon.games/images/gunnies-logo.png",
      "contractAddress": "0x...",
      "chain": 3344,
      "standard": "ERC721",
      "count": 9093
    }
  ]
}

Fields: name (display name), logo, logoIsFallback, logoFilename, contractAddress, chain (chain ID), standard (ERC721 / ERC1155 / etc), and count (indexed token count). Results are ordered by count descending.

Logo is always an absolute URL. logo is a ready-to-render absolute URL — clients never need to prepend a base path. Resolution order: (1) the collection's curated logo if it is already a full http(s) URL (logoIsFallback: false); otherwise (2) the first synced token's image, a full CDN/Arweave URL (logoIsFallback: true). It is only null when the collection has no renderable token image yet (unsynced or missing image data). The raw curated value from the registry (which may be a bare filename like azuki.png with no resolvable host) is preserved separately in logoFilename for future use, but you should render logo, not logoFilename.

Collection Item Index

GET /api/v1/collection/{contract}/items

Returns every item (token) in a collection, paginated — the collection “item index.” This is the single source of truth for a collection's enumeration: ownership + names + cached-CDN images + transferable/SBT flags, keyed by tokenId.

For marketplaces & indexers: build your collection grid from this endpoint and join your own data (listings, prices, activity, rarity) by (contractAddress, tokenId). No consumer should re-crawl the chain for NFT ownership or images — nft-data is the source of truth (with image backfill + CDN), you maintain listing data only.
ParameterTypeRequiredDescription
pagequeryNoPage number (default 1)
limitqueryNoItems per page (default 50, max 200); echoed as pageSize
ownerqueryNoFilter to a single holder within the collection
includequeryNoComma-list: traits (embeds attributes[]) and/or rarity (embeds rarity{}). Omit for a lean payload. Use this to bulk-ingest attributes + rarity instead of per-token crawling.
transferablequeryNotrue = tradable only, false = soulbound/non-tradable only. Omit for all. See Tradability.

Example (bulk attributes + rarity, one call):

curl "https://nft-data.pentagon.games/api/v1/collection/0xed5af38...c544/items?limit=200&include=traits,rarity"

Response:

{
  "contractAddress": "0xBF01e486...766",
  "chain": 3344,
  "collection": "EtherFantasy PentaPet",
  "total": 10,
  "page": 1, "limit": 50, "pageSize": 50,
  "items": [
    {
      "tokenId": "10",
      "name": "Pudde #10",
      "image": "https://images.pfpvault.com/3344/0xbf01.../10.webp",   // cached CDN
      "imageOriginal": "https://api.etherfantasy.com/api/pets/image/10",
      "fileType": "image", "mediaType": "image",
      "standard": "ERC721",
      "transferable": false,
      "owner": "0x03ae56d4...9294",
      "updatedAt": "2026-07-12T07:30:39+00:00"
    }
  ]
}

Page through with page/limit until you have all total items. Returns 404 if the contract isn't tracked.

Tradability & Soulbound

Every item carries a transferable boolean. nft-data is the source of truth for whether an NFT can be traded — marketplaces should depend on this rather than deriving it.

ValueMeaningMarketplace behavior
transferable: trueStandard tradable NFT (default)Listable / buyable normally
transferable: falseSoulbound / locked — cannot be transferredDo NOT allow listing; hide or clearly flag any existing listing; block purchase
Example — Gunnies PFP: 9,003 of 9,093 are soulbound. Filter with ?transferable=false (soulbound only) or ?transferable=true (tradable only) on the item index. Use it to prevent buying a listing that's no longer tradable, or to power a “non-tradable” filter. The flag is present on the item index, single-token, and owner endpoints.

Rarity

Trait-frequency rarity is precomputed by nft-data (so every Pentagon surface shows the same rank for a token) and returned on the single-token endpoint and via ?include=rarity on the item index. It ranks within the collection; rank 1 = rarest.

"rarity": {
  "rank": 763,            // 1 = rarest, within the collection
  "totalSupply": 10000,
  "traitCount": 7,
  "rarityScore": 744.76,  // rarity.tools style: Σ (total / count(trait value))
  "infoContent": 34.16    // Σ −log2(freq) — less gameable than rarityScore
}

Two independent standards are exposed so consumers pick: rarityScore (familiar, rarity.tools-compatible) and infoContent (information-theoretic, harder to game by stuffing common traits). rarity is null for collections without indexed traits.

Rarity ranks; provenance and notability classify. Trait frequency is countable, so a rank is a fact. History (hold conviction, wash-trading) and culture (notability) are not a single order — a conviction-held common and an abandoned rare aren't “more or less rare,” they're different kinds of object. Those axes are on the roadmap as classifications (a label + flags + raw numbers), served beside rarity, once full on-chain transfer history is backfilled — never blended into one score.

Collection Stats

GET /api/v1/collection/:contract_address/stats

Returns ownership statistics for a collection including total supply, unique holder count, and top holders.

Response:

{
  "contract": "0xcDAD57bFc48E8373280C6dc3039C5169353B6879",
  "name": "BCSH OASYS",
  "total_supply": 10000,
  "unique_owners": 3421,
  "top_holders": [
    {"address": "0x...", "count": 150},
    {"address": "0x...", "count": 87}
  ]
}

Transfer History

GET /api/v1/nft/:contract_address/:token_id/transfers

Returns the full transfer history for a specific NFT, from mint to present.

Response:

{
  "transfers": [
    {
      "from": "0x0000000000000000000000000000000000000000",
      "to": "0x37224cfd71347da6f097c94b8649f9c552f803c9",
      "tx_hash": "0xabc123...",
      "block_number": 1234567,
      "timestamp": "2026-01-15T12:30:00Z"
    }
  ]
}

Report an Issue (flag this)

POST /api/v1/report

Lets any API-enabled site (e.g. a marketplace or explorer) forward an end-user “flag this” report about a data-quality issue — a broken or wrong image, missing metadata, wrong owner, etc. Each project can surface reporting to its own users however it likes; reports land in one shared queue for staff to triage. Requires an app key (submit from your backend, not the browser, so the key is not exposed).

Body (JSON):

FieldTypeRequiredDescription
contractAddressstringyesNFT contract address
typestringyesOne of: broken_image, wrong_image, missing_metadata, wrong_trait, wrong_owner, not_synced, other
chainintegernoChain ID (alias chainId)
tokenIdstringnoSpecific token; omit for a collection-level report
messagestringnoFree text, max 2000 chars
reporterstringnoWho reported (wallet, email, or username)

Example:

curl -X POST https://nft-data.pentagon.games/api/v1/report \
  -H "X-PG-App-Key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "chain": 3344,
    "contractAddress": "0x8F83c6122Dd4d275B53a7846B3D3dB29Cca1e698",
    "tokenId": "9549",
    "type": "broken_image",
    "message": "grey background, transparency not removed",
    "reporter": "0x37224cfd71347da6f097c94b8649f9c552f803c9"
  }'

Response:

{ "status": "ok", "reportId": 42 }

List reports (staff triage): GET /api/v1/report (also app-key gated). Filters: ?status=open&chain=3344&contract=0x..&type=broken_image&page=1&limit=50.

{
  "count": 1, "page": 1, "limit": 50,
  "reports": [
    { "id": 42, "chain": 3344, "contractAddress": "0x8f83...", "tokenId": "9549",
      "type": "broken_image", "message": "grey background...", "reporter": "0x3722...",
      "source": "nftmining", "status": "open", "createdAt": "2026-07-04T07:23:51Z" }
  ]
}

Error Responses

// 401 — Invalid or inactive app key
{"error": "Invalid or inactive app key"}

// 404 — NFT or collection not found
{"error": "NFT not found"}

// 429 — Rate limit exceeded
{"error": "Rate limit exceeded", "retry_after": 60}

Supported Chains

The NFT Data API tracks collections across 14 blockchain networks. Pentagon Chain contracts get real-time WebSocket tracking (sub-second updates). All other chains use block-range scanning with 2–6 minute sync intervals.

ChainChain IDSync ModeStatus
Pentagon3344Real-time + ScannerLive
Ethereum1ScannerLive
BSC56ScannerLive
Polygon137ScannerLive
Arbitrum42161ScannerLive
Core1116ScannerLive
OASYS248ScannerLive
Monad143ScannerLive
Avalanche43114ScannerLive
SKALE Nebula1482601649ScannerLive
Pentagon Chain advantage: Since Pentagon Games operates the chain RPC, Pentagon chain contracts get real-time WebSocket event tracking with zero rate limits — ownership updates within ~2 seconds of on-chain transfers.

Tracked Collections

49 collections are actively tracked. Here are the key Pentagon Chain collections. Use GET /api/v1/collections for the full list.

Pentagon Chain (3344)

CollectionContractType
BCSH OASYS0xcDAD57bFc48E8373280C6dc3039C5169353B6879ERC721
BCSH Vaelion0x35A31E23FB1AAD207Ad4075C52e981dC9165059bERC721
BCSH No_50x0A5FE002F2eD146415A1f4865DE1c180a39D599EERC721
BCSH Tamago0x2444D26cC268848f2B1bd837456537510F1aac81ERC721
BCSH Baiyi0xF8C869a5575f44fB9b68F670e6B158B30fB8Ccf5ERC721
GCN NFT0x42D97d553Ee71deF76a131e48Fc42BCf8da3B141ERC721
GCN Shards0xfd8276d1745761D3C5C55269fe4466FCD31D9dD3ERC1155
Gunnies PFP0x7a8a3236e3783E7cC33b97729378e31Cf14d3EbcERC721
PentaPets0xe6BdE156369D209C4d420E966541eE17093705B5ERC721
RugPull Art0xd77f88ef51b2589d132d6eb61068079f61dfe4a3ERC721
Gemry0xc05b96b89Ce46c306223E3f4c413891d17E1De70ERC721
PEGNAMES0xf97EB9f8293D1FD5587a809Eb74518c300738d07ERC721
EtherFantasy0x8F83c6122Dd4d275B53a7846B3D3dB29Cca1e698ERC721

Ethereum (1)

CollectionContractType
BCSH ETH0x53b719422f427Fe158f480Bfc3Cf32201e416F89ERC721
MoonbirdsSee GET /api/v1/collections for addresses
Azuki
Doodles
CLONE X
Cool Cats
Lil Pudgys
World of Women
Nakamigos
0N1 Force
+ moreFull list via API

Adding a New Collection

Want your collection tracked by the Pentagon NFT Data API? Contact the Pentagon Games team with:

Once registered, all four sync layers begin tracking your collection automatically. You'll receive an app key for API access.

How It Works

The NFT Data API is backed by a four-layer sync engine that guarantees ownership accuracy. Every layer cross-checks the others — if they disagree, on-chain truth wins.

📡 Layer 1: Real-Time Listener

WebSocket subscriptions to on-chain Transfer events. Ownership updates within ~2 seconds. Currently active for Pentagon Chain contracts.

🔎 Layer 2: Block-Range Scanner

Periodic eth_getLogs() scanning from saved block checkpoint to chain head. Runs every 2–6 minutes. Covers all 14 chains. Never misses blocks — resumes from checkpoint even after downtime.

✅ Layer 3: On-Chain Verifier

Periodic ownerOf() spot checks comparing database state to on-chain ground truth. Catches edge cases like contract upgrades or admin transfers.

🖼 Layer 4: Image Cache Pipeline

Background image processor that downloads original artwork, converts to optimized 512px WebP, and uploads to the CDN. Detects same-image collections for efficient batch processing. Serves from CloudFront edge for sub-100ms global delivery.

Triangulation

When layers disagree on ownership:

Result: Triple-verified ownership data with optimized image delivery. No stale records, no missed transfers, no silent failures — and images load in milliseconds from the CDN edge.

Integration Examples

Python
JavaScript
Discord Bot
cURL
import requests

API_BASE = "https://nft-data.pentagon.games"
APP_KEY = "pk_live_your_key"
HEADERS = {"X-PG-App-Key": APP_KEY}

def get_nfts(wallet, chain_id=None):
    """Fetch all NFTs owned by a wallet."""
    url = f"{API_BASE}/api/v1/nft/owner/{wallet}"
    params = {}
    if chain_id:
        params["chain_id"] = chain_id
    resp = requests.get(url, headers=HEADERS, params=params)
    resp.raise_for_status()
    return resp.json()

def get_nft_detail(contract, token_id):
    """Fetch metadata for a specific NFT."""
    url = f"{API_BASE}/api/v1/nft/{contract}/{token_id}"
    resp = requests.get(url, headers=HEADERS)
    resp.raise_for_status()
    return resp.json()

# Example: List all Pentagon chain NFTs for a wallet
wallet = "0x37224cFD71347Da6f097c94B8649f9C552f803c9"
data = get_nfts(wallet, chain_id=3344)
print(f"Found {data['total']} NFTs on Pentagon Chain")
for nft in data["results"]:
    print(f"  {nft['collection']} #{nft['token_id']} - {nft.get('name', 'N/A')}")
const API_BASE = "https://nft-data.pentagon.games";
const APP_KEY = "pk_live_your_key";

async function getNFTs(wallet, chainId) {
  const params = new URLSearchParams();
  if (chainId) params.set("chain_id", chainId);

  const res = await fetch(
    `${API_BASE}/api/v1/nft/owner/${wallet}?${params}`,
    { headers: { "X-PG-App-Key": APP_KEY } }
  );
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

async function getNFTDetail(contract, tokenId) {
  const res = await fetch(
    `${API_BASE}/api/v1/nft/${contract}/${tokenId}`,
    { headers: { "X-PG-App-Key": APP_KEY } }
  );
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

// Example: Check if a wallet holds any Gunnies PFPs
const GUNNIES = "0x7a8a3236e3783E7cC33b97729378e31Cf14d3Ebc";
const wallet = "0x37224cFD71347Da6f097c94B8649f9C552f803c9";
const data = await getNFTs(wallet, 3344);
const gunnies = data.results.filter(n => n.contract.toLowerCase() === GUNNIES.toLowerCase());
console.log(`Wallet holds ${gunnies.length} Gunnies PFP(s)`);
// Discord.js bot — NFT verification command
const { SlashCommandBuilder } = require("discord.js");

const API_BASE = "https://nft-data.pentagon.games";
const APP_KEY = "pk_live_your_key";

module.exports = {
  data: new SlashCommandBuilder()
    .setName("verify-nft")
    .setDescription("Check NFT ownership for a wallet")
    .addStringOption(opt =>
      opt.setName("wallet").setDescription("Wallet address").setRequired(true)
    )
    .addStringOption(opt =>
      opt.setName("collection").setDescription("Collection name filter")
    ),

  async execute(interaction) {
    await interaction.deferReply();

    const wallet = interaction.options.getString("wallet");
    const collection = interaction.options.getString("collection");

    const params = new URLSearchParams();
    if (collection) params.set("collection", collection);

    const res = await fetch(
      `${API_BASE}/api/v1/nft/owner/${wallet}?${params}`,
      { headers: { "X-PG-App-Key": APP_KEY } }
    );
    const data = await res.json();

    if (data.total === 0) {
      return interaction.editReply("No NFTs found for this wallet.");
    }

    const lines = data.results.slice(0, 10).map(
      nft => `**${nft.collection}** #${nft.token_id}`
    );

    await interaction.editReply(
      `Found **${data.total}** NFT(s):\n${lines.join("\n")}`
    );
  }
};
# Health check
curl "https://nft-data.pentagon.games/"

# Look up NFTs by owner (public, no key)
curl "https://nft-data.pentagon.games/api/v1/nft/owner/0x37224cFD71347Da6f097c94B8649f9C552f803c9"

# Look up NFTs with app key + chain filter
curl -H "X-PG-App-Key: pk_live_your_key" \
  "https://nft-data.pentagon.games/api/v1/nft/owner/0x37224cFD71347Da6f097c94B8649f9C552f803c9?chain_id=3344"

# Single NFT detail
curl -H "X-PG-App-Key: pk_live_your_key" \
  "https://nft-data.pentagon.games/api/v1/nft/0xcDAD57bFc48E8373280C6dc3039C5169353B6879/4201"

# List all tracked collections
curl -H "X-PG-App-Key: pk_live_your_key" \
  "https://nft-data.pentagon.games/api/v1/collections"

# Collection stats
curl -H "X-PG-App-Key: pk_live_your_key" \
  "https://nft-data.pentagon.games/api/v1/collection/0xcDAD57bFc48E8373280C6dc3039C5169353B6879/stats"

# Transfer history
curl -H "X-PG-App-Key: pk_live_your_key" \
  "https://nft-data.pentagon.games/api/v1/nft/0xcDAD57bFc48E8373280C6dc3039C5169353B6879/4201/transfers"

Image Resolution

The API uses a four-tier image fallback to ensure NFTs always display the best available artwork:

1. Cached CDN Image Preferred

Pre-rendered 512×512 WebP images served from images.pfpvault.com via CloudFront. Optimized to ~30–50 KB per token (vs multi-MB originals). Includes correct artwork for upgraded/variant NFTs (Dark Setsuko, Obelith, etc). 150K+ NFTs covered and growing.

2. Project-Hosted Thumbnails

For collections that maintain their own thumbnail servers (e.g. Google Cloud Storage buckets), the cacher pulls from these fast, reliable sources instead of slow on-chain storage. Falls back here when CDN cache misses.

Example: Killer GF on-chain metadata points to Arweave (5.9 MB PNGs, frequent timeouts). The project hosts optimized thumbnails at storage.googleapis.com/kgf-thumbnails/{id}.jpg — the cacher uses these instead for reliable, fast ingestion.

3. On-Chain Metadata Image

Image URL from the token's on-chain metadata (tokenURI → JSON → image). May return a generic base image for collections with upgradeable metadata. Sources include IPFS, Arweave, and project APIs.

4. Original Image

Fallback to the original image captured at first sync. Used when all other tiers are unavailable.

CDN Image Spec

Smart Caching

The image cacher detects when a collection shares a single source image across all tokens (common with PFP passes and hero collections). In these cases, the image is downloaded and converted once, then server-side copied to all token slots — caching thousands of tokens in seconds rather than hours.

For collections with variant artwork (e.g. BCSH OASYS with Setsuko, Dark Setsuko, and Obelith Setsuko), the cacher groups tokens by source image and handles each variant independently.

Why this matters: Some collections (like BCSH OASYS / Setsuko) have upgradeable NFTs where the on-chain metadata returns a generic base image for all variants. The cached CDN layer stores the correct per-token artwork including upgraded variants.

Original Image Archival

When the cacher downloads a source image, it stores the pre-conversion original in a separate S3 path before resizing to the CDN WebP. This gives us a full-fidelity copy that doesn't depend on external sources staying online.

Image Change History Coming Soon

The system will track dated snapshots of NFT images over time. When the cacher detects that a token's source image has changed (e.g. an NFT upgrade, evolution, or metadata update), it preserves the previous version with a timestamp.

Metadata Refresh Coming Soon

A queued metadata refresh system is planned for handling upgradeable NFT collections:

Metadata & Licensing

TL;DR: Every NFT we serve carries a normalized metadata override built by our resolver, not just whatever the contract returns. On top of that we expose three top-level fields integrators care about: transferable (SBT vs tradable), license (machine-readable rights tier per the ERC-721L standard), and storage (where the image and metadata actually live and how permanent they are).

The Metadata Override Layer

For every collection we support, the API returns a metadata override — a normalized metadata object that we build and serve, regardless of what the underlying contract exposes. This mirrors the four-tier image system: the contract's tokenURI is one possible input, but it is not the source of truth our API hands back.

This matters because many collections (Gunnies PFP included) never wired working on-chain metadata — their tokenURI is empty or returns a generic base record. The override layer fills that gap with consistent, indexer-friendly fields across all 49 collections.

LayerSourceWhen used
1. Override (primary)Our resolver + pg_nft_dbAlways returned in metadata. Normalized, fast, cached.
2. Project / on-chainContract tokenURI or project APISurfaced under source.tokenUri for live verification when the contract exposes one.
Why an override and not just tokenURI: it lets us serve correct, current metadata for upgradeable NFTs, collections with broken or missing on-chain metadata, and game NFTs whose state changes off-chain — all through one consistent API shape.

Transferable (SBT) Flag

The top-level transferable boolean tells you whether a token is freely tradable (true) or soulbound/locked (false). It is promoted out of the trait list for easy filtering, and also kept inside metadata.attributes as a Transferable trait for backward compatibility.

"transferable": false   // soulbound / locked
"transferable": true    // freely tradable

For collections using a tiered-transfer model (e.g. Gunnies PFP: locked by default, unlockable per token), this flag reflects each token's current state.

Licensing (ERC-721L)

The license object exposes machine-readable usage rights following the ERC-721L licensing-aware NFT standard. It lets wallets, marketplaces, games, and AI agents reliably understand what a holder is permitted to do with the content — commercial use, derivatives, exclusivity, and so on.

"license": {
  "schema": "erc721l-metadata-v1",
  "rights_model": "base-plus-upgrade",
  "current_tier": 4,
  "current_name": "COMMERCIAL_DERIVS_NONEXCLUSIVE",
  "base_tier": 2,
  "upgraded_tier": 4,
  "terms_uri": "https://nft-data.pentagon.games/licenses/tier-4",
  "source": "override"
}
FieldMeaning
current_tier / current_nameThe effective rights tier for the token's current state (uint8 + canonical name).
rights_modelsingle-tier, base-plus-upgrade, dynamic-tier, or metadata-only.
base_tier / upgraded_tierFor tiered-transfer collections: rights in the locked vs unlocked state. Rights never decrease on upgrade (upgraded_tier >= base_tier).
terms_uriCanonical human-readable license terms for the tier.
sourceonchain if the contract implements ERC-721L (EIP-165 detected, on-chain getters win); override if served from our resolver mirror.

Canonical Tier Ladder (0–7)

TierNameRights
0UNSET / UNKNOWNNo rights declared — treat as no permission.
1PERSONAL_USEPersonal display / identity use only. No commercial, no derivatives.
2COMMERCIAL_NO_DERIVSCommercial use of the unmodified artwork. No derivatives/remixes.
3NONCOMMERCIAL_DERIVSDerivatives allowed for non-commercial purposes only.
4COMMERCIAL_DERIVS_NONEXCLUSIVECommercial derivatives allowed; non-exclusive grant.
5COMMERCIAL_DERIVS_EXCLUSIVEExclusive commercial derivatives rights.
6COMMERCIAL_DERIVS_SHAREALIKECommercial derivatives; downstream works must stay share-alike.
7CC0Public domain / unrestricted use.
Example — Gunnies PFP (tiered transfer + ERC-721L): A locked/soulbound Gunnie is tier 2 (COMMERCIAL_NO_DERIVS) — the holder may use the exact artwork commercially but may not make derivatives. Once unlocked into a transferable NFT it becomes tier 4 (COMMERCIAL_DERIVS_NONEXCLUSIVE) — the holder may create and commercially exploit derivatives. Both states permit commercial use under their respective conditions, satisfying the standard's rule that transferable rights are never lower than locked rights.

For AI Agents

Treat the license tier as a permission boundary. Before generating, publishing, selling, or remixing content derived from an NFT, read license.current_tier and verify the action is allowed: display is fine for tiers 1–7, commercial use of unmodified art for tiers 2/4/5/6/7, derivatives for tiers 3–7 (commercial only for 4–7). If an action isn't permitted, refuse and surface the minimum tier required.

Collection License Provenance (official links)

Every collection also carries research-verified license provenance — the license's official name and the canonical URL published by the project itself (never a blog or aggregator). This is served on /api/v1/collections items and on the single-token endpoint, right alongside contractAddress:

"license": {
  "type": "commercial",          // cc0 | commercial | limited_commercial | personal
                                 // | proprietary | copyleft | custom | unknown
  "name": "Azuki IP License",    // human-readable license name
  "url": "https://www.azuki.com/license",   // official license/terms page (project's own domain)
  "notes": "Confirm Elementals/Beanz enumerated"  // one-line nuance, may be null
},
"officialUrl": "https://www.azuki.com"       // project main site
ValueMeaning
license: nullRights are unverified — clients must NOT assume any usage rights.
type: unknownA license is known to exist but its terms/URL could not be verified (e.g. project wound down).
copyleftDerivatives allowed but must carry the same license (e.g. Milady's Viral Public License).
limited_commercialCommercial use with caps/conditions (e.g. Doodles' revenue-capped holder license).
Nuance matters: several collections are commonly mislabeled. Moonbirds was CC0 (Aug 2022) but has since been relicensed under PROOF terms; Nakamigos is often called CC0 but its official site grants CryptoPunks-style commercial rights instead. The notes field carries these corrections — trust license.url over community folklore.

Storage

The storage field tells integrators not just what the image and metadata are, but where they live and how permanent they are — so you can reason about reliability and durability.

"storage": {
  "image": {
    "tier": "cached",            // cached | project | onchain | original
    "provider": "s3+cloudfront", // s3+cloudfront | gcs | arweave | ipfs | external
    "permanence": "managed",     // managed | arweave | ipfs | external
    "url": "https://images.pfpvault.com/3344/0xcdad.../5555000000553.webp",
    "archivalOriginal": "s3://pg-nft-images/originals/3344/0xcdad.../5555000000553.png"
  },
  "metadata": {
    "tier": "override",          // override | onchain-tokenuri | project-api
    "provider": "pg_nft_db",
    "tokenUri": "https://api.bcsh.xyz/metadata/5555000000553"
  }
}
FieldMeaning
image.tierWhich of the four image fallback tiers served this token (see Image Resolution).
image.providerUnderlying storage backend the image was served from.
image.permanencemanaged (our S3/CDN, we control uptime), arweave/ipfs (decentralized permanent), or external (a third-party source that could change or disappear).
image.archivalOriginalOur full-fidelity pre-conversion copy in S3 — insurance against external sources going offline.
metadata.tierWhere the served metadata came from: our override, the contract tokenUri, or a project-api.
Why this matters: an integrator can decide whether to trust an image URL long-term. managed and arweave are safe to hotlink; external should be re-fetched or cached locally because the source may vanish. For any token where the external source dies, our archivalOriginal keeps a recoverable copy.

Mutable NFTs & Refresh Patterns

TL;DR: Most NFTs are static after mint — their metadata never changes, so we index once and serve forever. A few collections are mutable post-mint: a token's traits, image, transferable/SBT state, or license tier can change without the token ever being transferred. Our Transfer-driven sync is blind to those changes by design, so mutable collections need an explicit refresh trigger. This section defines the two mutation types and how each is kept current.

Why Transfer-Based Sync Isn't Enough

The core sync engine (see How It Works) keys off ownership change — it reacts to Transfer events and re-checks ownerOf(). That is the right model for ownership, but a post-mint upgrade changes a token's state, not its owner. No transfer fires, so nothing re-indexes, and the cached metadata captured at first ingest goes stale. The fix is a separate refresh path, triggered by something other than a transfer.

The Two Mutation Types

Type A — On-Chain State ChangeType B — Backend-Only Change
ExampleGunnies PFP upgrade (SBT → transferable NFT)Setsuko / BCSH OASYS variant upgrade (base → Dark / Obelith)
What changesAn on-chain flag flips (e.g. isUpgraded(tokenId) becomes true). Owner unchanged.The project's own metadata API starts returning a new image / traits. Nothing changes on-chain.
Where truth livesOn-chain, via a project-provided read function (selector).The project's metadata endpoint only.
How we verifyWe call the read function ourselves and trust the chain. A callback is only a hint to re-check.No on-chain verification possible. We re-pull the project endpoint and overwrite our cache.
Trigger optionsCallback (verified) or an approved tx-scanning cron backstop.Callback only — there is nothing on-chain to watch or scan.

Refresh Triggers

1. Project Callback (preferred)

After a qualifying action completes (an upgrade tx confirms, or a backend upgrade finalizes), the project's frontend or backend notifies us with just the contract and token id. We enqueue the refresh (RabbitMQ) and run the appropriate recipe for that collection. This is a push model — no polling, and no guessing which token changed, because the project tells us.

# Type A — on-chain upgrade (we re-verify on-chain before writing)
POST /api/v1/nft/notify_upgrade
Authorization: project app key (scoped to the collection)
{ "contract": "0x7a8a3236...3Ebc", "tokenId": "550", "chainId": 3344 }

# Type B — backend-only upgrade (we re-pull the project metadata endpoint)
POST /api/v1/nft/notify_refresh
Authorization: project app key (scoped to the collection)
{ "contract": "0x10d2d273...9a01", "tokenId": "232", "chainId": 3344 }
RuleType AType B
AuthenticationRequired (project app key)Required (project app key)
ScopeKey may only refresh its own collectionKey may only refresh its own collection
Trust modelCallback is a hint; on-chain read is authoritativeTrust-based; project endpoint is the only source
Rate limitQueued, deduped per (token, 10 min)Queued, deduped per (token, 10 min)

2. Tx-Scanning Cron (Type A backstop)

For Type A collections, an upgrade is a real on-chain write even when it emits no event (the call simply flips a storage flag). A project-specific cron scans the upgrade contract's transactions (not just Transfer logs), decodes the affected token id from calldata, and re-runs the on-chain read. This guarantees we catch upgrades even if a callback is never sent. Because it requires project-specific decoding logic and a read selector, each Type A collection submits its recipe for review and, once approved, gets a registered cron under a project-specific directory. Type B has no cron — there is nothing on-chain to scan.

3. User-Initiated Refresh

A best-effort, rate-limited self-serve path (POST /api/v1/nft/refresh/:contract/:tokenId, 1 per token per user per 10 min) lets a holder force a re-check from a project UI. It runs the same recipe as the callback. Useful as a fallback, but the project callback is preferred because it fires automatically at the exact moment state changes.

Refresh Recipe

Each mutable collection is described by a small recipe that the refresh worker reads to know how to re-derive state:

{
  "collection": "Gunnies PFP",
  "mutable": true,
  "type": "A",                       // A = on-chain read, B = project-api pull
  "refresh_source": "web3",          // web3 | project_api
  "read_fn": "isUpgraded(uint256)",  // Type A: on-chain read function
  "selector": "0x82f11a1a",          // Type A: function selector
  "maps_to_trait": "Transferable",   // how the read result is stored
  "api_url_template": null           // Type B: ".../{collection}/{id}"
}
Integrator takeaway: if you build on a mutable collection (Gunnies PFP, Setsuko/BCSH OASYS), read state from this API rather than caching it yourself, and call the appropriate notify_* endpoint right after your upgrade completes so our copy — and everything downstream (marketplaces, mining, games) — reflects the new state immediately.

Support & Contact

💬 Discord

Join the Pentagon Games server for developer support and announcements.

discord.gg/pentagongamesxp

✉️ Email

For app key requests, collection listings, and partnership inquiries.

nftprof@pentagon.games

📱 Telegram

Pentagon Games community channel.

t.me/pentagongamesxp