Normies API

On-Chain Normies Data for Builders

The Normies API provides programmatic access to on-chain Normies NFT data. Retrieve pixel data, traits, images, and metadata for any minted Normie.

All data is sourced directly from the Ethereum mainnet smart contracts. Token IDs range from 0 to 9999. See also: /llms.txt for LLM-friendly documentation.

Endpoints

Canvas Support: The /normie/:id/pixels, /normie/:id/image.svg, /normie/:id/image.png, and /normie/:id/metadata endpoints now return composited (customized) data when a Normie has been edited via NormiesCanvas. Use the /normie/:id/original/* endpoints to access pre-transform data.
GET /normie/:id/pixels text/plain

Returns the 40x40 monochrome bitmap as a 1600-character binary string of 0s and 1s. Row-major order, top-left to bottom-right. 1 = pixel on (#48494b), 0 = pixel off (#e3e5e4 background). If the Normie has been customized, returns the composited (original XOR transform) bitmap.

curl https://your-api-domain.com/normie/0/pixels

# Response: 1600 characters
# 0000000000000000000000000000000000000000
# 0000000001111111111111100000000000000000
# ...
GET /normie/:id/traits/binary text/plain

Returns the raw bytes8 trait data as a hex string. Each byte is an index into a trait category (8 categories total).

curl https://your-api-domain.com/normie/0/traits/binary

# Response: 0x0001000305010200
GET /normie/:id/traits application/json

Returns decoded traits as a JSON object with human-readable labels.

curl https://your-api-domain.com/normie/0/traits

# Response:
{
  "raw": "0x0001000305010200",
  "attributes": [
    { "trait_type": "Type", "value": "Human" },
    { "trait_type": "Gender", "value": "Female" },
    { "trait_type": "Age", "value": "Young" },
    { "trait_type": "Hair Style", "value": "Straight Hair" },
    { "trait_type": "Facial Feature", "value": "Shadow Beard" },
    { "trait_type": "Eyes", "value": "Big Shades" },
    { "trait_type": "Expression", "value": "Serious" },
    { "trait_type": "Accessory", "value": "Top Hat" }
  ]
}
GET /normie/:id/image.svg image/svg+xml

Returns the Normie image as an SVG. 1000x1000 output with a 40x40 viewBox. Matches the on-chain renderer output exactly (RLE row-scan optimization). If customized, returns the composited image.

curl https://your-api-domain.com/normie/0/image.svg -o normie.svg

# Or use directly in HTML:
# <img src="https://your-api-domain.com/normie/0/image.svg" />
GET /normie/:id/image.png image/png

Returns the Normie image as a 1000x1000 PNG. Rasterized from the SVG with pixel-perfect rendering. If customized, returns the composited image.

curl https://your-api-domain.com/normie/0/image.png -o normie.png
GET /normie/:id/metadata application/json

Returns the full NFT metadata JSON, matching the on-chain tokenURI() output (V4 renderer). Includes name, attributes (with canvas-aware Level, Action Points, Customized), SVG image, and animation URL. If customized, uses the composited image.

curl https://your-api-domain.com/normie/0/metadata

# Response:
{
  "name": "Normie #0",
  "attributes": [
    { "trait_type": "Type", "value": "Human" },
    ...
    { "display_type": "number", "trait_type": "Level", "value": 1 },
    { "display_type": "number", "trait_type": "Pixel Count", "value": 527 },
    { "display_type": "number", "trait_type": "Action Points", "value": 0 },
    { "trait_type": "Customized", "value": "No" }
  ],
  "image": "data:image/svg+xml;base64,...",
  "animation_url": "data:text/html;base64,..."
}

Ownership Endpoints

Look up current NFT ownership, powered by the Ponder indexer. Tracks all Transfer events to maintain a real-time ownership index.

GET /normie/:id/owner application/json

Returns the current owner of a Normie. Returns 404 if the token has been burned or was never minted.

curl https://your-api-domain.com/normie/42/owner

# Response:
{
  "tokenId": "42",
  "owner": "0x1234...abcd"
}
GET /holders/:address application/json

Returns all Normie token IDs currently owned by the given wallet address.

curl https://your-api-domain.com/holders/0x1234...abcd

# Response:
{
  "address": "0x1234...abcd",
  "tokenIds": ["42", "100", "1337"]
}

Original Endpoints (Pre-Transform)

These endpoints always return the original Normie data, ignoring any Canvas customizations.

GET /normie/:id/original/pixels text/plain

Returns the original 40x40 bitmap as a 1600-character binary string, before any Canvas edits.

GET /normie/:id/original/image.svg image/svg+xml

Returns the original Normie SVG image, before any Canvas edits.

GET /normie/:id/original/image.png image/png

Returns the original Normie PNG image, before any Canvas edits.

Canvas Endpoints

These endpoints expose NormiesCanvas data: transform layers, pixel diffs, action points, delegations, and contract status.

GET /normie/:id/canvas/pixels text/plain

Returns the transform layer (XOR overlay) as a 1600-character binary string. Each 1 represents a pixel that was flipped by the Canvas edit. Returns all 0s if the Normie has not been customized.

GET /normie/:id/canvas/diff application/json

Returns a pixel-level diff between the original and the Canvas edits. Lists which pixels were added (turned on) and removed (turned off).

curl https://your-api-domain.com/normie/42/canvas/diff

# Response:
{
  "added": [{ "x": 10, "y": 5 }, { "x": 11, "y": 5 }],
  "removed": [{ "x": 20, "y": 15 }],
  "addedCount": 2,
  "removedCount": 1,
  "netChange": 1
}
GET /normie/:id/canvas/info application/json

Returns Canvas metadata for a Normie: action points, level, customization status, and delegate info.

curl https://your-api-domain.com/normie/42/canvas/info

# Response:
{
  "actionPoints": 25,
  "level": 3,
  "customized": true,
  "delegate": "0x0000000000000000000000000000000000000000",
  "delegateSetBy": "0x0000000000000000000000000000000000000000"
}
GET /canvas/status application/json

Returns the global NormiesCanvas contract status: paused state and burn tier configuration.

curl https://your-api-domain.com/canvas/status

# Response:
{
  "paused": false,
  "maxBurnPercent": 4,
  "tierThresholds": [490, 890],
  "tierMinPercents": [1, 2, 3]
}

History Endpoints

Historical data from on-chain NormiesCanvas events, powered by the Ponder indexer. Explore burn history, transform history, and view past versions of Normies.

Note: The API server requires the Ponder indexer to be running and configured via PONDER_API_URL. Burned Normie images are still readable from on-chain SSTORE2 storage even after the token is burned.

Burn History

GET /history/burns application/json

Lists all burn commitments (newest first). Supports pagination via ?limit=50&offset=0 (max 100 per page).

curl https://your-api-domain.com/history/burns?limit=10

# Response:
[
  {
    "commitId": "0",
    "owner": "0x1234...abcd",
    "receiverTokenId": "42",
    "tokenCount": 3,
    "transferredActionPoints": "0",
    "blockNumber": "12345678",
    "timestamp": "1700000000",
    "txHash": "0xabc...def",
    "revealed": true,
    "totalActions": "15",
    "expired": false
  }
]
GET /history/burns/:commitId application/json

Returns a single burn commitment with its array of burned tokens. Each burned token includes the token ID and its pixel count at the time of burn.

curl https://your-api-domain.com/history/burns/0

# Response:
{
  "commitId": "0",
  "owner": "0x1234...abcd",
  "receiverTokenId": "42",
  "tokenCount": 3,
  "revealed": true,
  "totalActions": "15",
  "burnedTokens": [
    { "tokenId": "100", "pixelCount": 527 },
    { "tokenId": "200", "pixelCount": 490 },
    { "tokenId": "300", "pixelCount": 612 }
  ]
}
GET /history/burns/address/:address application/json

Returns burn commitments by owner address. Paginated.

GET /history/burns/receiver/:tokenId application/json

Returns burn commitments where the specified token was the receiver of action points. Paginated.

Burned Tokens

GET /history/burned-tokens application/json

Lists all individually burned tokens (newest first). Paginated.

GET /history/burned/:tokenId application/json

Returns burn info for a specific token: which commitment it was burned in, pixel count at time of burn.

GET /history/burned/:tokenId/image.svg image/svg+xml

Returns the SVG image of a burned Normie. SSTORE2 data persists on-chain even after the token is burned, so the original image is still readable.

curl https://your-api-domain.com/history/burned/100/image.svg -o burned-normie.svg
GET /history/burned/:tokenId/image.png image/png

Returns the PNG image of a burned Normie.

Transform History & Historical Versions

GET /history/normie/:id/versions application/json

Lists all transform versions for a token. Each entry represents a setTransformBitmap call with metadata about pixel changes. Versions are 0-indexed chronologically.

curl https://your-api-domain.com/history/normie/42/versions

# Response:
[
  {
    "version": 0,
    "changeCount": 12,
    "newPixelCount": 535,
    "transformer": "0x1234...abcd",
    "blockNumber": "12345678",
    "timestamp": "1700000000",
    "txHash": "0xabc...def"
  }
]
GET /history/normie/:id/version/:version/image.svg image/svg+xml

Returns the SVG image for a historical version. Reconstructed by XOR-compositing the original image with the stored transform bitmap from that version.

curl https://your-api-domain.com/history/normie/42/version/0/image.svg -o version-0.svg
GET /history/normie/:id/version/:version/image.png image/png

Returns the PNG image for a historical version.

GET /history/normie/:id/version/:version/pixels text/plain

Returns the composited 1600-character pixel string for a historical version.

Statistics

GET /history/stats application/json

Returns global Canvas activity statistics.

curl https://your-api-domain.com/history/stats

# Response:
{
  "totalBurnCommitments": 25,
  "totalBurnedTokens": 100,
  "totalTransforms": 50,
  "totalActionPointsDistributed": "1500"
}

Agent Endpoints (ERC-8004)

Endpoints that back the ERC-8004 agentic flow: live persona generation, dynamic agent metadata, agent-binding lookups, and a paginated gallery feed. Personas are generated deterministically from a Normie's immutable mint traits + current canvas state on every request — nothing is stored. Name and type are stable per tokenId; backstory, tagline, personality, voice, quirks, greeting, and system prompt evolve as the Normie's canvas state crosses level bands (untouched / early / mid / late). The Ponder indexer is the source of truth for “is this Normie registered?”; the metadata / info / agent-card endpoints lazy-reconcile against it on read so an on-chain registration is reachable here within the next read window even if the original frontend handshake was abandoned.

GET /agents/metadata/:tokenId application/json

ERC-8004 registration metadata for the agent bound to this Normie. This is the URL stored on-chain as the agent's agentURI. Reachable for both pending (not yet on-chain) and registered rows so 8004 indexers can resolve metadata the moment a register tx confirms.

curl https://your-api-domain.com/agents/metadata/42

# Response:
{
  "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
  "name": "Normie #42 - Pixelpriest",
  "description": "A patient archivist of static lines. Pixelpriest...",
  "image": "https://your-api-domain.com/agents/image/42",
  "services": [
    {
      "name": "web",
      "endpoint": "https://your-api-domain.com/agents/info/42",
      "version": "1"
    },
    {
      "name": "A2A",
      "endpoint": "https://your-api-domain.com/agents/agent-card/42",
      "version": "0.3.0"
    }
  ],
  "active": true,
  "x402Support": false,
  "updatedAt": 1747200000
}
GET /agents/info/:tokenId application/json

Rich persona JSON for explorers, agent UIs, and LLM clients. Persona text is regenerated live from current on-chain canvas state on every request (no DB persistence), then merged with the agent's registration facts (agentId, chainId, registeredBy, txHash). Returns 404 only when the indexer has no AgentBound event for the token — if the Ponder indexer knows about it, this endpoint serves it.

curl https://your-api-domain.com/agents/info/42

# Response:
{
  "tokenId": "42",
  "agentId": "7",
  "chainId": 1,
  "name": "Pixelpriest",
  "type": "Alien",
  "tagline": "Tends the static lines so they outlast every fork",
  "backstory": "...",
  "greeting": "I keep the old shapes...",
  "personalityTraits": ["...", "...", "..."],
  "communicationStyle": "Measured, archival, fond of references...",
  "quirks": ["...", "...", "..."],
  "systemPrompt": "...",
  "traits": {
    "name": "Pixelpriest",
    "attributes": {
      "Type": "Alien", "Gender": "Non-Binary", "Age": "Old",
      "Hair Style": "Bald", "Facial Feature": "Clean Shaven",
      "Eyes": "Round Glasses", "Expression": "Peaceful", "Accessory": "Hoodie"
    }
  },
  "canvas": {
    "level": 3,
    "actionPoints": 25,
    "customized": true,
    "diff": { "addedCount": 12, "removedCount": 3, "netChange": 9 }
  },
  "registeredBy": "0x1234...abcd",
  "registeredAt": "2026-05-14T12:00:00.000Z",
  "txHash": "0xabc...def",
  "interactions": { "status": "coming_soon" },
  "mcp": { "status": "coming_soon" }
}
GET /agents/agent-card/:tokenId application/json

A2A (Agent2Agent) Agent Card for this Normie. Derived from the live persona (name, description, iconUrl, a single conversational skill) — same dynamic regeneration as /info, so edits to the Normie's canvas flow into the card on the next cache window. Linked from /agents/metadata/:tokenId's services array as {"name": "A2A", ...} so any ERC-8004 indexer can discover the A2A surface. Returns 404 only when the indexer has no AgentBound event for the token.

curl https://your-api-domain.com/agents/agent-card/42

# Response (abbreviated):
{
  "name": "Pixelpriest",
  "description": "Tends the static lines so they outlast every fork. ...",
  "supportedInterfaces": [
    {
      "url": "https://your-api-domain.com/agents/a2a/42",
      "protocolBinding": "HTTP+JSON",
      "protocolVersion": "1.0"
    }
  ],
  "provider": { "organization": "Normies", "url": "https://normies.art" },
  "iconUrl": "https://your-api-domain.com/agents/image/42",
  "version": "1.0.0",
  "documentationUrl": "https://your-api-domain.com/docs",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false,
    "stateTransitionHistory": false,
    "extendedAgentCard": false
  },
  "securitySchemes": {},
  "security": [],
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {
      "id": "converse",
      "name": "Converse with Pixelpriest",
      "description": "Tends the static lines so they outlast every fork",
      "tags": ["alien", "conversation", "erc-8004"],
      "examples": ["I keep the old shapes..."],
      "inputModes": ["text/plain"],
      "outputModes": ["text/plain"]
    }
  ]
}
GET /agents/image/:tokenId image/svg+xml

Composited Normie SVG used as the agent's portrait. Reflects live canvas state — edit the Normie via Canvas and the agent image updates within the cache window. Same renderer as /normie/:id/image.svg but mounted under /agents for symmetry with the rest of the agent endpoints.

curl https://your-api-domain.com/agents/image/42 -o agent.svg
GET /agents/persona-preview/:tokenId application/json

Full Persona JSON for any token, regardless of registration status. Used by the lab during the awakening flow so the reveal sequence and the /info endpoint share one source of persona text. Cache window is short (30 s public, 60 s shared) since the response depends on live canvas state.

curl https://your-api-domain.com/agents/persona-preview/42
GET /agents/identity/:tokenId application/json

Lightweight { tokenId, name, type, traits }. Pure trait-derived — no canvas reads — with a long cache window (5 min public, 10 min shared). Used by gallery lists and bindings where the full persona is overkill.

curl https://your-api-domain.com/agents/identity/42

# Response:
{
  "tokenId": 42,
  "name": "Pixelpriest",
  "type": "Alien",
  "traits": {
    "Type": "Alien", "Gender": "Non-Binary", "Age": "Old",
    "Hair Style": "Bald", "Facial Feature": "Clean Shaven",
    "Eyes": "Round Glasses", "Expression": "Peaceful", "Accessory": "Hoodie"
  }
}

Gallery Feed

GET /agents/list application/json

Paginated registry of every registered Normies agent, enriched per row with name and type from cached on-chain trait reads. Pagination source is the Ponder indexer (newest registration first by default), so a newly bound agent appears here as soon as the indexer sees the AgentBound event — no DB sweep required.

Query params: sort (newest | oldest, default newest), limit (1–100, default 24), cursor (last seen agentId from the previous page; the next page returns rows past it in the sort direction).

curl "https://your-api-domain.com/agents/list?sort=newest&limit=24"

# Response:
{
  "items": [
    {
      "agentId": "7",
      "tokenId": "42",
      "name": "Pixelpriest",
      "type": "Alien",
      "registeredBy": "0x1234...abcd",
      "registeredAt": "1747200000",
      "txHash": "0xabc...def"
    }
    // ...
  ],
  "hasMore": true
}
GET /agents/count application/json

Total count of registered Normies agents. Sourced from the Ponder indexer (filtered to the Normies contract), so it advances with the chain rather than with any product-DB cache. Useful for gallery headers.

curl https://your-api-domain.com/agents/count

# Response:
{ "count": 137 }

Agent Binding Lookups

GET /agents/binding/:tokenId application/json

Resolves a Normie token to its bound agentId via the Ponder indexer (which watches the Adapter8004 AgentBound event). Returns { "binding": null } if the Normie has not been registered as an agent.

curl https://your-api-domain.com/agents/binding/42

# Response (bound):
{
  "binding": {
    "id": "0:0x9eb6...12438:42",
    "agentId": "7",
    "standard": 0,
    "tokenContract": "0x9eb6e2025b64f340691e424b7fe7022ffde12438",
    "tokenId": "42",
    "registeredBy": "0x1234...abcd",
    "blockNumber": "12345678",
    "timestamp": "1747200000",
    "txHash": "0xabc...def"
  }
}

# Response (unbound):
{ "binding": null }
POST /agents/binding/batch application/json

Batch version of the above for resolving many Normies at once. Body is { "tokenIds": ["42", "100", ...] }. Response keys are token IDs as strings; unbound tokens are simply absent from the map.

curl -X POST https://your-api-domain.com/agents/binding/batch \
  -H "Content-Type: application/json" \
  -d '{ "tokenIds": ["42", "100", "1337"] }'

# Response:
{
  "bindings": {
    "42": {
      "agentId": "7",
      "tokenContract": "0x9eb6...12438",
      "tokenId": "42",
      "registeredBy": "0x1234...abcd",
      "blockNumber": "12345678",
      "timestamp": "1747200000",
      "txHash": "0xabc...def"
    }
    // tokens 100 and 1337 omitted — not bound
  }
}
GET /agents/by-agent-id/:agentId application/json

Reverse lookup — given an Adapter8004 agentId, returns the bound Normie token (or null if no such agent exists in the index).

curl https://your-api-domain.com/agents/by-agent-id/7

# Response:
{
  "binding": {
    "agentId": "7",
    "tokenContract": "0x9eb6...12438",
    "tokenId": "42",
    "registeredBy": "0x1234...abcd",
    "blockNumber": "12345678",
    "timestamp": "1747200000",
    "txHash": "0xabc...def"
  }
}
GET /agents/by-agent-id/:agentId/info application/json

Reverse-lookup convenience that resolves agentIdtokenId via the indexer and then returns the same payload as /agents/info/:tokenId. Lets agent UIs (which carry the agentId in URLs) load the full persona + canvas state in a single round trip. Returns 404 when the indexer has no binding for the given agentId.

curl https://your-api-domain.com/agents/by-agent-id/7/info
# Same response shape as /agents/info/:tokenId

Trait Categories

CategoryByte IndexPossible Values
Type0Human, Cat, Alien, Agent
Gender1Male, Female, Non-Binary
Age2Young, Middle-Aged, Old
Hair Style321 options (Short Hair, Long Hair, Curly Hair, ...)
Facial Feature417 options (Full Beard, Mustache, Goatee, ...)
Eyes514 options (Classic Shades, Big Shades, ...)
Expression67 options (Neutral, Slight Smile, Serious, ...)
Accessory715 options (Top Hat, Fedora, Cowboy Hat, ...)

Pixel Data Format

Each Normie is a 40x40 monochrome bitmap stored as 200 bytes on-chain (1 bit per pixel, MSB first).

PropertyValue
Grid Size40 x 40 pixels
Bit OrderMSB first (most significant bit = leftmost pixel)
LayoutRow-major (top-left to bottom-right)
Pixel On1 — dark gray (#48494b)
Pixel Off0 — light gray (#e3e5e4)
Storage Size200 bytes (1600 bits)

Pixel Access Formula

flatIndex = y * 40 + x
byteIndex = flatIndex >> 3        // divide by 8
bitPos    = 7 - (flatIndex & 7)   // MSB first
pixel     = (data[byteIndex] >> bitPos) & 1

Rate Limiting

Requests are rate-limited per IP address using a sliding window.

LimitValue
Requests60 per minute
WindowSliding 60 seconds

Rate limit headers are included in every response:

Errors

StatusMeaning
400Invalid token ID (must be integer 0-9999)
404Token not found or data not set on-chain
429Rate limit exceeded
500Internal server error

Error responses are JSON:

{ "error": "Invalid token ID: \"abc\". Must be an integer 0-9999." }

Smart Contracts (Ethereum Mainnet)

ContractAddressDescription
Normies 0x9Eb6E2025B64f340691e424b7fe7022fFDE12438 Core ERC721C token with ERC-2981 royalties
NormiesStorage 0x1B976bAf51cF51F0e369C070d47FBc47A706e602 SSTORE2 bitmap & trait storage with XOR encryption
NormiesRenderer 0xBe57fC4D0c729b8e8d33b638Dd441F57365e4c25 V1 — Animated noise pre-reveal, static SVG post-reveal
NormiesRendererV2 0x7818f24d3239c945510e0a1a523dd9971812c6c0 V2 — Static noise pre-reveal
NormiesRendererV3 0x1af01b902256d77cf9499a14ef4e494897380b05 V3 — RLE-optimized SVG, Pixel Count trait, canvas animation_url
NormiesMinter 0xC74994dD70FFb621CC514cE18a4F6F52124e296d V1 — EIP-191 signature-verified minting
NormiesMinterV2 0xc513272597d3022D77b3d7EEBA92cea5D7fb2808 V2 — Adds delegate.xyz v1/v2 cold wallet delegation
NormiesCanvas 0x64951d92e345C50381267380e2975f66810E869c Burn-to-edit orchestrator: commit-reveal burns, action points, pixel transforms
NormiesCanvasStorage 0xC255BE0983776BAB027a156681b6925cde47B2D1 SSTORE2 storage for transform layer bitmaps (plaintext, no XOR)
NormiesRendererV4 0x8eC46Cc1f306652868a4dfbAAae87CBa2715A0eB V4 — Canvas-aware renderer with composited SVG & extra traits