KRC-721 Indexer Overview

Overview

KRC-721 is a token standard for non-fungible tokens (NFTs) on the Kaspa network. It defines a set of rules and interfaces for creating, managing, and transferring unique digital assets.

Features

Restrictions

This indexer only supports IPFS CIDs for metadata and image URLs. All URLs must start with the ipfs:// prefix. While the indexer will accept any URL during deployment without validation, only tokens with IPFS-compliant URLs will be processed. All tools that enable KRC-721 token deployment must enforce this restriction to prevent invalid deployments.

Deployments

Indexer deployments are available for the following networks:

REST API Specifications

Documentation Index:

Kaspa Networks

The Kaspa network id must be specified in the URL path. Currently, the following networks ids are supported:

The indexer must be running on the same network as the network specified in the URL path. Specifying a different network id will result in an error.

Response Format

All responses are in JSON format. The response result is wrapped in a Response object with the following fields:

The message field is always present and contains a human-readable message describing the result of the request. The "success" text in the message field indicates that the request was successful.

If an error occurs (e.g. a resource is not found or some other error), the message field will contain an error message and the HTTP status code will be set appropriately (400 for bad requests, 404 for not found, 500 for server errors).

Error Handling: See ERROR_HANDLING.md for complete error handling documentation.

Checking that the message field contains "success" and the HTTP status code is 200 is a good way to check that the request was successful.

Serialization Notes

next field omission: The server uses serde(skip_serializing_if = "Option::is_none") for the next field. When there are no more pages, the next field is entirely absent from the response JSON (it is not set to null). Client code should check for the absence of the field, e.g. if (data.next != null) in JavaScript.

royaltyTo vs royalty_to: Collection listing endpoints (/nfts) use camelCase royaltyTo, while operation endpoints (/ops, /deployments, /ops/score/:score, /ops/txid/:txid) use snake_case royalty_to. This is a serialization inconsistency between different Rust structs on the server.

opScore type inconsistency: The /deployments?direction=backward endpoint may return opScore as a raw JSON number (e.g. 95527572848001), while /ops returns it as a string (e.g. "24907687056003"). Clients should handle both formats.

Pagination

IMPORTANT: Pagination is a critical feature. See PAGINATION.md for comprehensive documentation, examples, and common mistakes.

Quick Overview

Resource listing endpoints are paginated if the number of records exceeds the user-specified limit or the maximum default limit of 50 records.

Key Points:

Query Parameters

Offset Types by Endpoint

Different endpoints use different offset formats:

| Endpoint | Offset Type | Format | Example | |----------|-------------|--------|---------| | /nfts | Score | Numeric string | "24907845280005" | | /ops | Score | Numeric string | "95831359456003" | | /deployments | Score | Numeric string | "95474495640001" | | /history/{tick}/{id} | Score | Numeric string | "26236285306000" | | /owners/{tick} | TokenId | Numeric | 4 | | /address/{address}/{tick} | TokenId | Numeric | 10 | | /address/{address} | TickTokenOffset | String "TICK-tokenId" | "NACHO-10" |

Critical: The /address/{address} endpoint uses a string format "TICK-tokenId" (with hyphen), not a number!

Basic Example

// Fetch all pages
let offset = null;
let allItems = [];

while (true) {
  const url = offset 
    ? `/api/v1/krc721/mainnet/nfts?offset=${offset}`
    : `/api/v1/krc721/mainnet/nfts`;
  
  const response = await fetch(url);
  const data = await response.json();
  
  allItems.push(...(data.result || []));
  
  if (data.next == null) break;  // No more pages (field absent or null)
  offset = data.next;             // Use next value as offset
}

Direction

Use direction=backward to get the latest items first without pagination.

Limits

Common Mistakes

  1. Wrong: Manually incrementing offset (offset += 50) Correct: Use next value from response

  2. Wrong: Using numeric offset for /address/{address} endpoint Correct: Use TickTokenOffset format "TICK-tokenId"

  3. Wrong: Checking data.next === null only Correct: Check data.next == null (handles both null and undefined/absent field)

For complete pagination documentation with examples in multiple languages, see PAGINATION.md.

REST endpoints

Indexer Status

GET /api/v1/krc721/{network}/status

Response:

{
    "message": "success",
    "result": {
        "version": "2.0.0",
        "network": "mainnet",
        "isNodeConnected": true,
        "isNodeSynced": true,
        "isIndexerSynced": true,
        "lastKnownBlockHash": "10f410db94ba152fa9c4159a60fcbd1a35ee29155d15267a4fc2a11037f5765c",
        "blueScore": 386440622,
        "currentOpScore": 95837274503999,
        "daaScore": 388293377,
        "powFeesTotal": 137690523684666,
        "royaltyFeesTotal": 4433059192903052,
        "tokenDeploymentsTotal": 310,
        "tokenMintsTotal": 118178,
        "tokenTransfersTotal": 196483
    }
}

Collections

Get Collections List

GET /api/v1/krc721/{network}/nfts

Query Parameters:

Pagination: This endpoint uses Score-based pagination. See PAGINATION.md for details.

Field Reference: See FIELD_REFERENCE.md for field descriptions.

Response:

{
    "message": "success",
    "result": [
        {
            "deployer": "kaspa:qph4jwnrwgfd690e3a5zrwdeetfkpauxu2nku0jasgnp794kp4rr5taw7erxg",
            "royaltyTo": "kaspa:qph4jwnrwgfd690e3a5zrwdeetfkpauxu2nku0jasgnp794kp4rr5taw7erxg",
            "buri": "ipfs://bafybeiau2zfmq6o2gvw7g5rerwem7fa2bd6prm3hvyx6kn42is7yq26sey",
            "max": "10000",
            "royaltyFee": "179900000000",
            "daaMintStart": "0",
            "premint": "500",
            "tick": "KSPR",
            "txIdRev": "3b15799a3e3041fe692890352f9d60de5e487c6f6d63ee5c2310b872ba5b49af",
            "mtsAdd": "1738336231427",
            "minted": "1267",
            "opScoreMod": "58790365648007",
            "state": "deployed",
            "mtsMod": "1759297835433",
            "opScoreAdd": "24907687056003"
        }
    ],
    "next": 24907845280005
}

Note: Collection list results use camelCase royaltyTo. The next field is omitted when there are no more pages.

Get Collection Details

GET /api/v1/krc721/{network}/nfts/{tick}

Response:

{
    "message": "success",
    "result": {
        "deployer": "kaspa:qr5e65mqknfnsa6d486axdtcqpredz6hfseet2x56u7nh25mmpyazgsmdp9y8",
        "royaltyTo": "kaspa:qr5e65mqknfnsa6d486axdtcqpredz6hfseet2x56u7nh25mmpyazgsmdp9y8",
        "buri": "ipfs://bafybeifwjyipfzlorzaw42amf53lvo3x6hpfg5xhr6km5drjmucsftrfy4",
        "max": "10000",
        "royaltyFee": "212000000000",
        "daaMintStart": "102045126",
        "premint": "0",
        "tick": "NACHO",
        "txIdRev": "145e07ae3278f3d6753ea33b22c66767f2d7a4c002fcfec117c75306dec527e2",
        "mtsAdd": "1738336906070",
        "minted": "10000",
        "opScoreMod": "26885172096005",
        "state": "deployed",
        "mtsMod": "1746310927897",
        "opScoreAdd": "24907847264002"
    }
}

Tokens

Get Token Details

GET /api/v1/krc721/{network}/nfts/{tick}/{id}

Response:

{
    "message": "success",
    "result": {
        "tick": "NACHO",
        "tokenId": "1",
        "owner": "kaspa:qps6zry9swaqjf5xehae5nsga2sx84xmzes2gc5y8lznaj4t6gm9w7klxrzq5",
        "opScoreMod": "26236285306000"
    }
}

Get Token Owners

GET /api/v1/krc721/{network}/owners/{tick}

Query Parameters:

Pagination: This endpoint uses TokenId-based pagination. See PAGINATION.md for details.

Response:

{
    "message": "success",
    "result": [
        {
            "tick": "NACHO",
            "tokenId": "1",
            "owner": "kaspa:qps6zry9swaqjf5xehae5nsga2sx84xmzes2gc5y8lznaj4t6gm9w7klxrzq5",
            "opScoreMod": "26236285306000"
        },
        {
            "tick": "NACHO",
            "tokenId": "2",
            "owner": "kaspa:qznft0mg03nlvj5lu0xv4w4ddxffzsut4m7z3tucq0vm7jeh5urt2dpg8cgk0",
            "opScoreMod": "91455826760001"
        },
        {
            "tick": "NACHO",
            "tokenId": "3",
            "owner": "kaspa:qplfvwnt6ywwjz76d3358anh0kuymmamfr8ec86907rpfjygzp0ex4d283q3d",
            "opScoreMod": "67448378712007"
        }
    ],
    "next": 4
}

Address Holdings

Get Address NFT List

GET /api/v1/krc721/{network}/address/{address}

Query Parameters:

CRITICAL: This endpoint uses TickTokenOffset format ("NACHO-10"), NOT a numeric offset!

Pagination: See PAGINATION.md for complete details and examples.

Response:

{
    "message": "success",
    "result": [
        {
            "tick": "NACHO",
            "buri": "ipfs://bafybeifwjyipfzlorzaw42amf53lvo3x6hpfg5xhr6km5drjmucsftrfy4",
            "tokenId": "3",
            "opScoreMod": "67448378712007"
        },
        {
            "tick": "NACHO",
            "buri": "ipfs://bafybeifwjyipfzlorzaw42amf53lvo3x6hpfg5xhr6km5drjmucsftrfy4",
            "tokenId": "4",
            "opScoreMod": "67448354408006"
        },
        {
            "tick": "NACHO",
            "buri": "ipfs://bafybeifwjyipfzlorzaw42amf53lvo3x6hpfg5xhr6km5drjmucsftrfy4",
            "tokenId": "8",
            "opScoreMod": "70142421312002"
        }
    ],
    "next": "NACHO-10"
}

Note: The next field is a string in "TICK-tokenId" format, not a number. The buri field is included in address listing results.

Get Address Collection Holdings

GET /api/v1/krc721/{network}/address/{address}/{tick}

Query Parameters:

Pagination: This endpoint uses TokenId-based pagination.

Response:

{
    "message": "success",
    "result": [
        {
            "tick": "NACHO",
            "tokenId": "3",
            "opScoreMod": "67448378712007"
        },
        {
            "tick": "NACHO",
            "tokenId": "4",
            "opScoreMod": "67448354408006"
        },
        {
            "tick": "NACHO",
            "tokenId": "8",
            "opScoreMod": "70142421312002"
        }
    ],
    "next": 10
}

Operations

Get Operations List

GET /api/v1/krc721/{network}/ops

Query Parameters:

Pagination: This endpoint uses Score-based pagination. Use direction=backward to get latest operations first.

Response (deploy operation):

{
    "message": "success",
    "result": [
        {
            "p": "krc-721",
            "deployer": "kaspa:qph4jwnrwgfd690e3a5zrwdeetfkpauxu2nku0jasgnp794kp4rr5taw7erxg",
            "royalty_to": "kaspa:qph4jwnrwgfd690e3a5zrwdeetfkpauxu2nku0jasgnp794kp4rr5taw7erxg",
            "tick": "KSPR",
            "txIdRev": "3b15799a3e3041fe692890352f9d60de5e487c6f6d63ee5c2310b872ba5b49af",
            "mtsAdd": "1738336231427",
            "op": "deploy",
            "opData": {
                "buri": "ipfs://bafybeiau2zfmq6o2gvw7g5rerwem7fa2bd6prm3hvyx6kn42is7yq26sey",
                "max": "10000",
                "royaltyFee": "179900000000",
                "daaMintStart": "0",
                "premint": "500"
            },
            "opScore": "24907687056003",
            "feeRev": "600000000000"
        }
    ],
    "next": 24907713096004
}

Response (transfer operation, via direction=backward):

{
    "message": "success",
    "result": [
        {
            "p": "krc-721",
            "deployer": "kaspa:qq5fysv96t636u4slda59daza6tn5j5p5x5953hs6dstajuw0u6l6cyj4d0ef",
            "to": "kaspa:qplfvwnt6ywwjz76d3358anh0kuymmamfr8ec86907rpfjygzp0ex4d283q3d",
            "tick": "NACHO",
            "txIdRev": "82569c3254315597026e9521cc358b1847bf29bd71fe4a0a6218c7d22764a13a",
            "mtsAdd": "1774247106390",
            "op": "transfer",
            "opData": {
                "tokenId": "1439"
            },
            "opScore": "95832809264001",
            "feeRev": "101624"
        }
    ],
    "next": 95831359456003
}

Response (mint operation):

{
    "p": "krc-721",
    "deployer": "kaspa:qr92zkpt83n08fmw50dssekaa4jwrzs5uqtv8j9xl2l4fhschnfvcj6pedllk",
    "to": "kaspa:qr92zkpt83n08fmw50dssekaa4jwrzs5uqtv8j9xl2l4fhschnfvcj6pedllk",
    "tick": "KASBRICKS",
    "txIdRev": "830877ca8adbd5649757dbba606d640f3e481710eaccaa4b9d8b71cba3fc39c2",
    "mtsAdd": "1774246561832",
    "op": "mint",
    "opData": {
        "tokenId": "327",
        "royalty": {
            "royaltyFee": "5000000000"
        }
    },
    "opScore": "95831422944002",
    "feeRev": "1000011378"
}

Note: Operation results use snake_case royalty_to (not royaltyTo). The opError field only appears on failed operations. The to field appears on mint and transfer operations. The royalty object inside opData appears on mint operations when a royalty was paid.

Get Deployments List

GET /api/v1/krc721/{network}/deployments

Query Parameters:

Pagination: This endpoint uses Score-based pagination.

Response:

{
    "message": "success",
    "result": [
        {
            "deployer": "kaspa:qpw73mje2uw5xjvrk0ttuyl3ejd0lp3ty07zzdqsn0ga5v7zypkt5h5mcrw68",
            "royalty_to": "kaspa:qypsx5q89kulr9t0fuandpmhwuur7t6kqhc0qpaqctcm393fqyl0c5gfpn3xr53",
            "buri": "ipfs://Qmbrt6vchnFerZ4fPDaeGvTa3jXVX8nf1nLnmPHqSP12z4",
            "max": "218",
            "royaltyFee": "42000000000",
            "daaMintStart": "0",
            "premint": "0",
            "tick": "DAGSKULK",
            "txIdRev": "21e8ba72f60baf19c8e1b5c0e4197e3439bd9586add336dfc7f8af8592b9341f",
            "mtsAdd": "1774123975590",
            "opScore": 95527572848001
        }
    ],
    "next": 95474495640001
}

Note: Deployments use snake_case royalty_to. The opScore field may appear as a raw JSON number (not a string) in deployment results -- this is a serialization inconsistency. The state field is not present in deployment operation results.

Get Operation Details by Score

GET /api/v1/krc721/{network}/ops/score/{score}

Response:

{
    "message": "success",
    "result": {
        "p": "krc-721",
        "deployer": "kaspa:qph4jwnrwgfd690e3a5zrwdeetfkpauxu2nku0jasgnp794kp4rr5taw7erxg",
        "royalty_to": "kaspa:qph4jwnrwgfd690e3a5zrwdeetfkpauxu2nku0jasgnp794kp4rr5taw7erxg",
        "tick": "KSPR",
        "txIdRev": "3b15799a3e3041fe692890352f9d60de5e487c6f6d63ee5c2310b872ba5b49af",
        "mtsAdd": "1738336231427",
        "op": "deploy",
        "opData": {
            "buri": "ipfs://bafybeiau2zfmq6o2gvw7g5rerwem7fa2bd6prm3hvyx6kn42is7yq26sey",
            "max": "10000",
            "royaltyFee": "179900000000",
            "daaMintStart": "0",
            "premint": "500"
        },
        "opScore": "24907687056003",
        "feeRev": "600000000000"
    }
}

Get Operation Details by Transaction ID

GET /api/v1/krc721/{network}/ops/txid/{txid}

Response: Same structure as ops/score/{score} above. Returns the operation associated with the given transaction ID.

Get Royalty Fee

GET /api/v1/krc721/{network}/royalties/{address}/{tick}

Returns the royalty fee (in SOMPI) that a specific address must pay to mint from a collection. This is not paginated.

Response:

{
    "message": "success",
    "result": "212000000000"
}

Note: The result is a string representing SOMPI (1 SOMPI = 10^-8 KAS). In this example, 212000000000 SOMPI = 2120 KAS.

Get Rejection Reason by Transaction ID

Rejections include the reason for which the indexer has rejected the transaction. Rejections can occur due to an invalid operation (insufficient fee, invalid ticker, already deployed ticker, etc.) or due to a static check failure (e.g. missing "to" field in the transfer operation).

A transaction is recorded in the indexer log only if it contains a valid krc721 envelope (all other transactions are ignored).

GET /api/v1/krc721/{network}/rejections/txid/{txid}

Response (when rejection exists):

{
    "message": "success",
    "result": "InsufficientFee"
}

Response (when no rejection exists): HTTP 404 with Content-Type: text/plain body:

not found

Note: This endpoint returns text/plain (not JSON) for 404 responses.

Reserved Tickers

GET /api/v1/krc721/{network}/reserved

Returns the list of reserved ticker names that cannot be deployed.

Response:

{
    "message": "success",
    "result": [
        "KII",
        "AED",
        "EUR",
        "IGRA",
        "CAD",
        "KAS",
        "KASPA",
        "USDC",
        "KEF",
        "NACHO",
        "USD",
        "USDT"
    ]
}

Get Ownership History

GET /api/v1/krc721/{network}/history/{tick}/{id}

Get ownership history of a token (all ownership changes).

Query Parameters:

Pagination: This endpoint uses Score-based pagination.

Response:

{
    "message": "success",
    "result": [
        {
            "owner": "kaspa:qps6zry9swaqjf5xehae5nsga2sx84xmzes2gc5y8lznaj4t6gm9w7klxrzq5",
            "opScoreMod": "26236285306000",
            "txIdRev": "d9533a0604a57b24148a9d16d2d5c24e95f27f5bf968f628ca580a64d1e3000b"
        }
    ]
}

Note: When this is the only/last page, the next field is omitted entirely from the response.

Get Available Token ID Ranges

GET /api/v1/krc721/{network}/ranges/{tick}

Get available token ID ranges for minting in a collection.

Response (when ranges are available):

{
    "message": "success",
    "result": "100,50,200,20,5000,99990"
}

The result string represents available ranges in format start1,size1,start2,size2,.... For each range, start is the starting token ID and size is how many consecutive token IDs are available starting from that ID.

Response when fully minted:

{
    "message": "success",
    "result": ""
}

Note: NACHO (10000/10000 minted) returns an empty string, confirming the collection is fully minted.

Pagination Guide

Overview

The KRC-721 API uses cursor-based pagination to efficiently retrieve large datasets. Understanding pagination is crucial for working with collections, operations, and other list endpoints.

How Pagination Works

Basic Concept

  1. Request a page: Make a request to a paginated endpoint (optionally with offset, limit, and direction parameters)
  2. Check for more data: Look for the next field in the response
  3. Fetch next page: If next is present, use its value as the offset parameter for the next request
  4. Repeat: Continue until next is null or undefined

Response Structure

All paginated responses follow this structure:

{
  "message": "success",
  "result": [...],  // Array of items
  "next": "offset_value"  // null if no more pages
}

Key Points:

Offset Types

Different endpoints use different offset formats. This is critical to understand.

1. Score-Based Offsets (Numeric)

Format: Unsigned 64-bit integer (string representation)

Used by:

Example:

{
  "message": "success",
  "result": [...],
  "next": "13000000000000"
}

Usage:

# First page
GET /api/v1/krc721/mainnet/nfts

# Next page (using next from previous response)
GET /api/v1/krc721/mainnet/nfts?offset=13000000000000

2. TokenId-Based Offsets (Numeric)

Format: Unsigned 64-bit integer (string representation)

Used by:

Example:

{
  "message": "success",
  "result": [...],
  "next": "51"
}

Usage:

# First page
GET /api/v1/krc721/mainnet/owners/MYCOLLECTION

# Next page
GET /api/v1/krc721/mainnet/owners/MYCOLLECTION?offset=51

3. TickTokenOffset-Based Offsets (String)

Format: "TICK-tokenId" (tick symbol, hyphen, token ID)

Used by:

Example:

{
  "message": "success",
  "result": [...],
  "next": "FOO-123"
}

Usage:

# First page
GET /api/v1/krc721/mainnet/address/kaspa:abc123...

# Next page
GET /api/v1/krc721/mainnet/address/kaspa:abc123...?offset=FOO-123

Important: The format is TICK-tokenId, not TICK_tokenId or TICK:tokenId. Use a hyphen!

Query Parameters

All paginated endpoints support these query parameters:

offset (optional)

The cursor for pagination. Use the next value from the previous response.

limit (optional)

Number of records to return per page.

direction (optional)

Direction of iteration.

Note: These parameters work together. You can combine them:

Complete Examples

Example 1: Fetching All Collections (Score-Based)

JavaScript/TypeScript:

async function fetchAllCollections(network: string) {
  const collections = [];
  let offset: string | null = null;
  let hasMore = true;

  while (hasMore) {
    const url = offset 
      ? `https://krc721.kat.foundation/api/v1/krc721/${network}/nfts?offset=${offset}`
      : `https://krc721.kat.foundation/api/v1/krc721/${network}/nfts`;
    
    const response = await fetch(url);
    const data = await response.json();
    
    if (data.result) {
      collections.push(...data.result);
    }
    
    // Check if there's a next page
    hasMore = data.next !== null && data.next !== undefined;
    offset = data.next;
  }

  return collections;
}

Python:

import requests

def fetch_all_collections(network: str):
    collections = []
    offset = None
    base_url = f"https://krc721.kat.foundation/api/v1/krc721/{network}/nfts"
    
    while True:
        url = f"{base_url}?offset={offset}" if offset else base_url
        response = requests.get(url)
        data = response.json()
        
        if data.get("result"):
            collections.extend(data["result"])
        
        # Check if there's a next page
        if not data.get("next"):
            break
        
        offset = data["next"]
    
    return collections

cURL:

# First page
curl "https://krc721.kat.foundation/api/v1/krc721/mainnet/nfts"

# Response includes: "next": "13000000000000"

# Second page
curl "https://krc721.kat.foundation/api/v1/krc721/mainnet/nfts?offset=13000000000000"

# Continue until "next" is null

Example 2: Fetching All NFTs for an Address (TickTokenOffset-Based)

JavaScript/TypeScript:

async function fetchAllAddressNFTs(network: string, address: string) {
  const nfts = [];
  let offset: string | null = null;
  let hasMore = true;

  while (hasMore) {
    const url = offset
      ? `https://krc721.kat.foundation/api/v1/krc721/${network}/address/${address}?offset=${offset}`
      : `https://krc721.kat.foundation/api/v1/krc721/${network}/address/${address}`;
    
    const response = await fetch(url);
    const data = await response.json();
    
    if (data.result) {
      nfts.push(...data.result);
    }
    
    hasMore = data.next !== null && data.next !== undefined;
    offset = data.next;
  }

  return nfts;
}

Important: Notice the offset format is "FOO-123" (string with hyphen), not a number!

Example 3: Backward Pagination (Latest First)

JavaScript/TypeScript:

async function fetchLatestDeployments(network: string, limit: number = 10) {
  const url = `https://krc721.kat.foundation/api/v1/krc721/${network}/deployments?direction=backward&limit=${limit}`;
  const response = await fetch(url);
  const data = await response.json();
  return data.result || [];
}

Note: When using direction=backward, you get the most recent items first. No offset needed for the first page.

Example 4: Fetching with Custom Limit

async function fetchWithLimit(network: string, limit: number = 20) {
  // Limit is capped at 50, so 20 is valid
  const url = `https://krc721.kat.foundation/api/v1/krc721/${network}/nfts?limit=${limit}`;
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

Common Mistakes

❌ Mistake 1: Using Wrong Offset Format

Wrong:

// Using numeric offset for address endpoint
const offset = 123;
fetch(`/api/v1/krc721/mainnet/address/${address}?offset=${offset}`);

Correct:

// Using TickTokenOffset format
const offset = "FOO-123";
fetch(`/api/v1/krc721/mainnet/address/${address}?offset=${offset}`);

❌ Mistake 2: Not Checking for Null

Wrong:

while (data.next) {  // This fails if next is null!
  // ...
}

Correct:

while (data.next !== null && data.next !== undefined) {
  // ...
}
// Or simply:
while (data.next) {
  // But be careful - empty string "" is falsy!
}

❌ Mistake 3: Incrementing Offset Manually

Wrong:

let offset = 0;
while (true) {
  const data = await fetch(`/api/v1/krc721/mainnet/nfts?offset=${offset}`);
  offset += 50;  // NO! Don't do this!
  // ...
}

Correct:

let offset = null;
while (true) {
  const url = offset 
    ? `/api/v1/krc721/mainnet/nfts?offset=${offset}`
    : `/api/v1/krc721/mainnet/nfts`;
  const data = await fetch(url);
  offset = data.next;  // Use the next value from response
  if (!offset) break;
}

❌ Mistake 4: Assuming Offset Type

Wrong:

// Assuming all offsets are numbers
const offset = parseInt(data.next);

Correct:

// Use the offset as-is (it's already a string)
const offset = data.next;

Endpoint-Specific Notes

Collections List (/nfts)

Operations List (/ops)

Deployments List (/deployments)

Address NFT List (/address/{address})

Token Owners (/owners/{tick})

Ownership History (/history/{tick}/{id})

Performance Considerations

  1. Limit Size: Use appropriate limits (default 50 is usually good)
  2. Caching: Consider caching paginated results
  3. Parallel Requests: Don't make parallel requests with the same offset
  4. Rate Limiting: Be aware of rate limits (if configured)

Testing Pagination

Use the sandbox (/sandbox) to test pagination:

  1. Navigate to a paginated endpoint
  2. Enter query parameters: limit=10&direction=forward
  3. Execute the request
  4. Copy the next value
  5. Use it as offset in the next request

Summary

For endpoint-specific details, see REST.md.

API Field Reference

Complete reference for all fields in KRC-721 API responses.

Common Response Fields

Response Wrapper

All API responses follow this structure:

| Field | Type | Description | Example | |-------|------|-------------|---------| | message | string | Human-readable message describing the result | "success" or "not found" | | result | object or array | The actual response data (null if error) | See endpoint-specific docs | | next | string or null | Pagination offset for next page (null if no more pages) | "13000000000000" or null |

Note: When message is "success" and HTTP status is 200, the request succeeded.

Indexer Status Fields

Endpoint: /api/v1/krc721/{network}/status

| Field | Type | Description | Example | |-------|------|-------------|---------| | version | string | Indexer version | "2.0.0" | | network | string | Network identifier | "mainnet" or "testnet-10" | | isNodeConnected | boolean | Whether Kaspa node is connected | true | | isNodeSynced | boolean | Whether Kaspa node is synced | true | | isIndexerSynced | boolean | Whether indexer is synced | true | | lastKnownBlockHash | string or null | Last known block hash (hex) | "abc123..." or null | | blueScore | string | Current blue score (uint64 as string) | "1312860" | | currentOpScore | string | Current operation score (uint64 as string) | "13000000000000" | | daaScore | string | Current DAA score (uint64 as string) | "1312860" | | powFeesTotal | string | Total PoW fees collected (SOMPI, uint64 as string) | "100000000000" | | royaltyFeesTotal | string | Total royalty fees collected (SOMPI, uint64 as string) | "50000000000" | | tokenDeploymentsTotal | string | Total number of collections deployed (uint64 as string) | "150" | | tokenMintsTotal | string | Total number of tokens minted (uint64 as string) | "50000" | | tokenTransfersTotal | string | Total number of transfers (uint64 as string) | "12000" |

Collection Fields

Collection List Item (/nfts)

| Field | Type | Description | Example | |-------|------|-------------|---------| | tick | string | Collection ticker symbol (1-10 alphanumeric) | "FOO" | | deployer | string | Deployer Kaspa address | "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya" | | royaltyTo | string or null | Royalty beneficiary address (if royalties enabled) | "kaspa:..." or null | | buri | string or null | Base URI for metadata (IPFS CID) | "ipfs://Qm..." or null | | metadata | object or null | Inscribed metadata object (if not using buri) | See Metadata Object below | | max | string | Maximum supply (uint64 as string) | "1000" | | minted | string | Number of tokens minted (uint64 as string) | "456" | | premint | string | Number of pre-minted tokens (uint64 as string) | "10" | | daaMintStart | string | DAA score when minting starts (uint64 as string) | "525037124" | | royaltyFee | string or null | Royalty fee per mint (SOMPI, uint64 as string) | "1000000000" or null | | state | string | Collection state | "deployed" | | txIdRev | string | Deployment transaction ID (reversed byte order, hex) | "0000000000000000000000000000000000000000000000000000000000000000" | | mtsAdd | string | Timestamp when collection was added (milliseconds, uint64 as string) | "1712808987852" | | mtsMod | string | Timestamp when collection was last modified (milliseconds, uint64 as string) | "1712808987852" | | opScoreAdd | string | Operation score when collection was deployed (uint64 as string) | "13000000000000" | | opScoreMod | string | Operation score when collection was last modified (uint64 as string) | "13000000000000" |

Collection Details (/nfts/{tick})

Same fields as Collection List Item, plus all fields are always present (not flattened).

Token Fields

Token (/nfts/{tick}/{id})

| Field | Type | Description | Example | |-------|------|-------------|---------| | tick | string | Collection ticker symbol | "FOO" | | tokenId | string | Token ID within collection (uint64 as string) | "123" | | owner | string | Current owner Kaspa address | "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqhqrxplya" | | buri | string or null | Base URI for token metadata (IPFS CID) | "ipfs://Qm..." or null | | metadata | object or null | Inscribed metadata (if collection uses inscribed metadata) | See Metadata Object below |

Note: Token metadata URI is typically {buri}/{tokenId} for external metadata, or uses collection's inscribed metadata.

Token Owner (/owners/{tick})

| Field | Type | Description | Example | |-------|------|-------------|---------| | tick | string | Collection ticker symbol | "FOO" | | tokenId | string | Token ID (uint64 as string) | "123" | | owner | string | Owner Kaspa address | "kaspa:..." | | opScoreMod | string | Operation score when ownership was last modified (uint64 as string) | "13000000000000" |

Address Holdings Fields

Address NFT Info (/address/{address})

| Field | Type | Description | Example | |-------|------|-------------|---------| | tick | string | Collection ticker symbol | "FOO" | | tokenId | string | Token ID (uint64 as string) | "123" | | buri | string or null | Base URI for metadata (IPFS CID) | "ipfs://Qm..." or null | | metadata | object or null | Inscribed metadata (if collection uses inscribed metadata) | See Metadata Object below | | opScoreMod | string | Operation score when ownership was last modified (uint64 as string) | "13000000000000" |

Address Collection Holdings (/address/{address}/{tick})

Same as Address NFT Info, but filtered to a specific collection.

Operation Fields

Operation (/ops, /ops/score/{id}, /ops/txid/{txid})

| Field | Type | Description | Example | |-------|------|-------------|---------| | p | string | Protocol identifier | "krc-721" | | op | string | Operation type | "deploy", "mint", "transfer", or "discount" | | tick | string | Collection ticker symbol | "FOO" | | deployer | string | Deployer address (for deploy operations) | "kaspa:..." | | royaltyTo | string or null | Royalty beneficiary (for deploy operations with royalties) | "kaspa:..." or null | | to | string or null | Recipient address (for mint/transfer operations) | "kaspa:..." or null | | opScore | string | Operation score (uint64 as string) | "13000000000000" | | txIdRev | string | Transaction ID (reversed byte order, hex) | "0000000000000000000000000000000000000000000000000000000000000000" | | mtsAdd | string | Timestamp when operation was added (milliseconds, uint64 as string) | "1712808987852" | | feeRev | string | Fee paid (SOMPI, uint64 as string) | "10000000000" | | opError | string or null | Error message if operation failed | "InsufficientFee" or null | | opData | object | Operation-specific data | See Operation Data below |

Operation Data (opData)

The opData field structure depends on the operation type:

Deploy Operation Data

| Field | Type | Description | Example | |-------|------|-------------|---------| | buri | string or null | Base URI for metadata (IPFS CID) | "ipfs://Qm..." or null | | metadata | object or null | Inscribed metadata object | See Metadata Object below | | max | string | Maximum supply (uint64 as string) | "1000" | | royaltyFee | string or null | Royalty fee per mint (SOMPI, uint64 as string) | "1000000000" or null | | daaMintStart | string | DAA score when minting starts (uint64 as string) | "525037124" | | premint | string | Number of pre-minted tokens (uint64 as string) | "10" |

Mint Operation Data

| Field | Type | Description | Example | |-------|------|-------------|---------| | tokenId | string | Token ID minted (uint64 as string) | "123" | | to | string | Recipient address | "kaspa:..." |

Transfer Operation Data

| Field | Type | Description | Example | |-------|------|-------------|---------| | tokenId | string | Token ID transferred (uint64 as string) | "123" | | to | string | Recipient address | "kaspa:..." |

Discount Operation Data

| Field | Type | Description | Example | |-------|------|-------------|---------| | to | string | Address receiving discount | "kaspa:..." | | discountFee | string | Discounted fee amount (SOMPI, uint64 as string) | "500000000" |

Ownership History Fields

History Entity (/history/{tick}/{id})

| Field | Type | Description | Example | |-------|------|-------------|---------| | owner | string | Owner Kaspa address at this point in history | "kaspa:..." | | opScoreMod | string | Operation score when ownership changed (uint64 as string) | "13000000000000" | | txIdRev | string | Transaction ID of the transfer (reversed byte order, hex) | "0000000000000000000000000000000000000000000000000000000000000000" |

Metadata Object

When a collection uses inscribed metadata (not buri), the metadata object structure is:

| Field | Type | Description | Example | |-------|------|-------------|---------| | name | string | Collection name | "Artsy Kaspa" | | description | string | Collection description | "Bring NFT to Kaspa" | | image | string | Collection image URI (IPFS CID) | "ipfs://Qm..." | | attributes | array or null | Array of attribute objects | See Attribute Object below |

Attribute Object

| Field | Type | Description | Example | |-------|------|-------------|---------| | traitType | string | Trait name/type | "Background", "Eyes", "Rarity" | | value | string | Trait value | "Blue", "Gold", "Rare" | | displayType | string or null | Display hint (optional) | "date", "boost_percentage", or null |

Special Field Explanations

Score Fields

Operation Scores (opScore, opScoreAdd, opScoreMod):

DAA Score (daaScore, daaMintStart):

Timestamp Fields

mtsAdd (Milliseconds Timestamp Added):

mtsMod (Milliseconds Timestamp Modified):

Transaction ID Fields

txIdRev (Transaction ID Reversed):

Why reversed? Internal database optimization for lexicographic ordering.

Amount Fields

All amounts are in SOMPI (10^-8 KAS):

State Fields

state (Collection State):

Collection State Values

Field Naming Conventions

Data Type Notes

Common Patterns

Checking for Optional Fields

// Check if field exists and has value
if (collection.royaltyTo) {
  // Royalties are enabled
}

// Check for null/undefined
if (collection.buri === null || collection.buri === undefined) {
  // Using inscribed metadata instead
}

Converting Amounts

// Convert SOMPI to KAS
function sompiToKas(sompi) {
  return parseFloat(sompi) / 100000000;
}

// Example
const royaltyFeeKas = sompiToKas(collection.royaltyFee); // 10 KAS

Working with Timestamps

// Convert milliseconds timestamp to Date
const addedDate = new Date(parseInt(collection.mtsAdd));

Parsing Available Ranges

// Parse available token ID ranges
function parseRanges(rangeString) {
  if (!rangeString) return []; // Fully minted
  
  const parts = rangeString.split(',');
  const ranges = [];
  for (let i = 0; i < parts.length; i += 2) {
    ranges.push({
      start: parseInt(parts[i]),
      size: parseInt(parts[i + 1])
    });
  }
  return ranges;
}

// Example: "100,50,200,20" = [{start: 100, size: 50}, {start: 200, size: 20}]

See Also

Error Handling Guide

Complete guide to handling errors when using the KRC-721 API.

HTTP Status Codes

The API uses standard HTTP status codes:

| Status Code | Meaning | When It Occurs | |-------------|---------|----------------| | 200 OK | Success | Request succeeded, message is "success" | | 400 Bad Request | Invalid Request | Invalid path parameters, malformed URLs | | 403 Forbidden | Access Denied | IP filtering enabled and IP not allowed | | 404 Not Found | Resource Not Found | Collection/token/operation doesn't exist | | 500 Internal Server Error | Server Error | Indexer error, database error, etc. | | 429 Too Many Requests | Rate Limited | Too many requests (if rate limiting enabled) |

Response Format

Success Response

{
  "message": "success",
  "result": { ... },
  "next": "..." // if paginated
}

Error Response

Error responses have two formats:

Format 1: JSON Error (for 400, 500)

{
  "message": "error message here",
  "result": null,
  "next": null
}

Format 2: Plain Text Error (for 404, some 500)

not found

Note: When result is null and message is "not found", HTTP status is 404.

Common Errors

Resource Not Found (404)

When: Requesting a collection, token, or operation that doesn't exist.

Response:

Example:

GET /api/v1/krc721/mainnet/nfts/INVALIDTICK
# Response: 404 Not Found
# Body: "not found"

Handling:

async function getCollection(network: string, tick: string) {
  const response = await fetch(`.../nfts/${tick}`);
  
  if (response.status === 404) {
    return null; // Not found
  }
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  
  const data = await response.json();
  return data.result || null;
}

Invalid Path Parameters (400)

When: Invalid URL path parameters (e.g., invalid tick format, malformed address).

Response:

Example:

{
  "message": "Failed to deserialize path parameters",
  "location": "tick"
}

Handling:

try {
  const response = await fetch(url);
  const data = await response.json();
  
  if (response.status === 400) {
    console.error('Invalid request:', data.message);
    if (data.location) {
      console.error('Error in field:', data.location);
    }
  }
} catch (error) {
  // Handle error
}

Network Errors

When: Connection failures, timeouts, DNS errors.

Handling:

async function safeFetch(url: string, timeoutMs: number = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeoutId);
    return response;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw new Error(`Network error: ${error.message}`);
  }
}

Rate Limiting (429)

When: Too many requests (if rate limiting is configured on the server).

Response:

Handling:

async function fetchWithRetry(url: string, maxRetries: number = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url);
    
    if (response.status === 429) {
      const retryAfter = response.headers.get('Retry-After');
      const delay = retryAfter ? parseInt(retryAfter) * 1000 : (i + 1) * 1000;
      
      console.log(`Rate limited, retrying after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }
    
    return response;
  }
  
  throw new Error('Max retries exceeded');
}

Server Errors (500)

When: Internal server error, database error, indexer error.

Response:

Handling:

async function handleServerError(response: Response) {
  const contentType = response.headers.get('content-type');
  
  if (contentType?.includes('application/json')) {
    const data = await response.json();
    throw new Error(`Server error: ${data.message}`);
  } else {
    const text = await response.text();
    throw new Error(`Server error: ${text}`);
  }
}

Error Handling Patterns

Pattern 1: Check HTTP Status First

async function apiCall(url: string) {
  const response = await fetch(url);
  
  // Check HTTP status
  if (response.status === 404) {
    return null; // Not found
  }
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  const data = await response.json();
  
  // Check API message
  if (data.message !== 'success') {
    throw new Error(`API Error: ${data.message}`);
  }
  
  return data.result;
}

Pattern 2: Comprehensive Error Handler

class ApiError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public apiMessage?: string,
    public response?: any
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

async function safeApiCall<T>(url: string): Promise<T | null> {
  try {
    const response = await fetch(url);
    const contentType = response.headers.get('content-type');
    
    // Handle different response types
    if (contentType?.includes('application/json')) {
      const data = await response.json();
      
      if (response.status === 404 || data.message === 'not found') {
        return null; // Not found
      }
      
      if (response.status !== 200 || data.message !== 'success') {
        throw new ApiError(
          `API Error: ${data.message}`,
          response.status,
          data.message,
          data
        );
      }
      
      return data.result;
    } else {
      // Plain text response (usually 404)
      const text = await response.text();
      
      if (response.status === 404) {
        return null;
      }
      
      throw new ApiError(
        `Server error: ${text}`,
        response.status,
        text
      );
    }
  } catch (error) {
    if (error instanceof ApiError) {
      throw error;
    }
    
    // Network errors
    throw new ApiError(
      `Network error: ${error instanceof Error ? error.message : 'Unknown'}`,
      undefined,
      undefined,
      error
    );
  }
}

Pattern 3: Retry Logic

async function fetchWithRetry<T>(
  url: string,
  options: {
    maxRetries?: number;
    retryDelay?: number;
    retryableStatuses?: number[];
  } = {}
): Promise<T> {
  const {
    maxRetries = 3,
    retryDelay = 1000,
    retryableStatuses = [429, 500, 502, 503, 504]
  } = options;
  
  let lastError: Error | null = null;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);
      
      if (!response.ok && retryableStatuses.includes(response.status)) {
        if (attempt < maxRetries) {
          const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }
      }
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      const data = await response.json();
      
      if (data.message !== 'success') {
        throw new Error(`API Error: ${data.message}`);
      }
      
      return data.result;
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));
      
      if (attempt < maxRetries && error instanceof Error) {
        // Check if error is retryable
        if (error.message.includes('timeout') || 
            error.message.includes('network') ||
            error.message.includes('ECONNREFUSED')) {
          const delay = retryDelay * Math.pow(2, attempt);
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }
      }
      
      throw lastError;
    }
  }
  
  throw lastError || new Error('Max retries exceeded');
}

Common Error Scenarios

Scenario 1: Collection Doesn't Exist

const collection = await getCollection('mainnet', 'INVALID');
if (collection === null) {
  console.log('Collection not found');
  // Handle gracefully
}

Scenario 2: Invalid Tick Format

try {
  const collection = await getCollection('mainnet', 'invalid-tick-with-dashes');
} catch (error) {
  if (error.statusCode === 400) {
    console.error('Invalid tick format (must be 1-10 alphanumeric)');
  }
}

Scenario 3: Network Timeout

try {
  const collections = await fetchAllCollections('mainnet');
} catch (error) {
  if (error.message.includes('timeout')) {
    console.error('Request timed out, try again later');
    // Implement retry logic
  }
}

Scenario 4: Rate Limited

try {
  const data = await fetchWithRetry(url, {
    maxRetries: 5,
    retryDelay: 2000
  });
} catch (error) {
  if (error.statusCode === 429) {
    console.error('Rate limited, please slow down requests');
    // Implement exponential backoff
  }
}

Best Practices

  1. Always check HTTP status codes before parsing JSON
  2. Check message === 'success' even when status is 200
  3. Handle 404 gracefully - it's a normal case (resource doesn't exist)
  4. Implement retry logic for network errors and 5xx errors
  5. Use exponential backoff for rate limiting
  6. Log errors with context for debugging
  7. Validate inputs before making requests
  8. Set timeouts on all requests
  9. Handle JSON parsing errors separately from network errors
  10. Provide user-friendly error messages

Error Response Examples

Example 1: Not Found

$ curl https://krc721.kat.foundation/api/v1/krc721/mainnet/nfts/INVALID

HTTP/1.1 404 Not Found
Content-Type: text/plain

not found

Example 2: Invalid Path Parameter

$ curl https://krc721.kat.foundation/api/v1/krc721/mainnet/nfts/invalid-tick-format

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "message": "Failed to deserialize path parameters",
  "location": "tick"
}

Example 3: Server Error

$ curl https://krc721.kat.foundation/api/v1/krc721/mainnet/status

HTTP/1.1 500 Internal Server Error
Content-Type: text/plain

Database connection error

Troubleshooting

Problem: Getting 404 for existing collection

Possible causes:

  1. Wrong network specified in URL
  2. Collection tick is case-sensitive
  3. Collection was recently deployed and indexer hasn't synced yet

Solution:

// Check indexer sync status first
const status = await fetch('/api/v1/krc721/mainnet/status').then(r => r.json());
if (!status.result.isIndexerSynced) {
  console.log('Indexer is still syncing, try again later');
}

Problem: Pagination returns same results

Possible causes:

  1. Using wrong offset format
  2. Manually incrementing offset instead of using next
  3. Offset format mismatch (using numeric for TickTokenOffset endpoint)

Solution: See PAGINATION.md for correct pagination implementation.

Problem: CORS errors in browser

Possible causes:

  1. Making requests from different origin
  2. Server CORS not configured

Solution: CORS is enabled by default. If issues persist, check server configuration.

Problem: Timeout errors

Possible causes:

  1. Network connectivity issues
  2. Server overloaded
  3. Request taking too long (default timeout: 10 seconds)

Solution: Implement retry logic with exponential backoff.

See Also

KRC-721 Standard Specifications

Data types

Restrictions

Security budget (PoW fees)

For a sustainable ecosystem, the security budget for Kaspa miners is ensured through the following mandatory operation fees:

Any transactions that do not meet the required security budget will be rejected by the indexer.

When pre-minting is enabled, the deployer must include a payment of at least 10 KAS per token in the deployment transaction fees. For example, if the deployer pre-mints 5 tokens, the deployment transaction must include a payment of 1050 KAS (1000 KAS for the deployment + 50 KAS for the pre-mint).

When a royalty fee is specified during deployment, each mint operation for this token must include a payment of at least royaltyFee to the royalty beneficiary. Specifically, the minting transaction must contain a first output paying at least royaltyFee SOMPI to the designated royalty beneficiary address.

The royalties fee set during deployment must be from 0.1 KAS to 10,000,000 KAS, inclusive.

Operations

Deploy

Deploys a new NFT collection.

Format

{
    "p": "krc-721",
    "op": "deploy",
    "tick": "string",
    "max": "uint64",
    "buri": "string", // optional URI
    "metadata": "object", // optional metadata object
    "royaltyFee": "uint64", // optional amount
    "royaltyTo": "string", // optional Kaspa beneficiary address
    "mintDaaScore": "uint64", // optional mint start daa score
    "premint": "uint64", // optional premint
    "to": "string" // optional deployer: gets the deployer permissions and premint amount
}

Deployer

The deployer defaults to the sender of the deploy operation.

If the optional to field is set, then the address within this field will be the only address with deployer permissions.

Royalties

When royalties are set during deployment, mints have to have a payment at least equal to royaltyFee to the royalties beneficiary to be considered valid.

The beneficiary Kaspa address royaltyOwner is optional; when royaltyFee is set alone, the default beneficiary is the deployer.

Mint royalties payment

The royalties payment has to be the first output payment of a mint reveal transaction.

Mint start time

After a NFT collection has been deployed, it can be minted right away.

This behaviour can be changed with the optional deploy parameter mintDaaScore of type uint64 that allows to specify a mint start time at given virtual Daa Score which is alike Kaspa's definition of time.

Pre-mint

A deployer can decide to pre-mint an amount of tokens using the premint parameter. The deployer will receive the pre-mint directly on deployment. The amount of tokens remaining available for standard mint is therefore max - premint.

Custom pre-mint receiver

The deployer can mark a custom Kaspa address as receiver of pre-mint tokens thanks to the to deploy operation parameter.

Metadata base URI

{
    "p": "krc-721",
    "op": "deploy",
    "tick": "kasparty",
    "max": "1000",
    "buri": "ipfs://...",
    "royaltyFee": "1000000000",
    "royaltyOwner": "kaspa:qqabb6cz...",
    "mintDaaScore": "525037124",
    "premint": 50,
    "to": "kaspa:qif906cz..."
}

Metadata inscribed on-chain

{
    "p": "krc-721",
    "op": "deploy",
    "tick": "kasparty",
    "max": "1000",
    "metadata": {
        "name": "Artsy Kaspa",
        "description": "Bring NFT to Kaspa",
        "image": "ipfs://...",
        "attributes": [
            {
                "traitType": "immutable", 
                "value": "permanent"
            },
            {
                "displayType": "booster_percentage", // optional display hint
                "traitType": "immutable booster", 
                "value": 30
            }
        ]
    },
    "royaltyFee": "1000000000",
    "royaltyOwner": "kaspa:qqabb6cz...",
    "mintDaaScore": "525037124"
}

All NFT items have the same attributes if immutable attributes were given, which is always the case with inscribed metadata.

NFT Metadata URI (off-chain)

The externally hosted NFT collection json file shall be accessed by visiting the metadata URI directly.

{
    "name": "Artsy Kaspa",
    "description": "Bring NFT to Kaspa",
    "image": "ipfs://...",
    "attributes": [
        {
            "traitType": "color",
            "value": "red|magenta|orange" // proposal: multiple possible values
        },
        {
            "traitType": "form",
            "value": "oval|circle|contour"
        }
    ]
}

An individual NFT item's attributes, shall be hosted at URI {buri}/{tokenid}.

The representation may vary according to marketplaces needs as it is off-chain data.

{
    "name": "Artsy Kaspa",
    "description": "Bring NFT to Kaspa",
    "tokenid": 3998,
    "image": "ipfs://...",
    "attributes": [
        {
            "traitType": "color", 
            "value": "magenta"
        },
        {
            "traitType": "form", 
            "value": "circle"
        }
    ]
}

Validation Rules

Mint

Mints a new token from an existing collection.

The receiver address "to" is optional, it defaults to operation sender account.

{
    "p": "krc-721",
    "op": "mint",
    "tick": "string",
    "to": "string" // optional Kaspa recipient address
}

Validation Rules

Transfer

Transfers ownership of a token.

{
    "p": "krc-721",
    "op": "transfer",
    "tick": "string",
    "id": "uint64",
    "to": "string" // Kaspa recipient address
}

Validation Rules

Discount

The deployer can assign mint discount to an address

{
    "p": "krc-721",
    "op": "discount",
    "tick": "string",
    "to": "string", // Kaspa discount recipient address
    "discountFee": "uint64",
}

Validation Rules

RUST Data Structures

UserOperation

The following UserOperation struct can be used to represent a user operation submitted to the KRC-721 indexer.



#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpDeploy {
    #[serde(rename = "p")]
    pub protocol: String,
    pub op: Op,
    pub tick: String,
    #[serde(flatten)]
    pub metadata: Metadata,
    #[serde_as(as = "DisplayFromStr")]
    pub max: u64,
    #[serde(default)]
    #[serde(rename = "royaltyTo")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub royalty_to: Option<String>,
    #[serde(default)]
    #[serde(rename = "royaltyFee")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde_as(as = "Option<DisplayFromStr>")]
    pub royalty_fee: Option<u64>,
    #[serde(rename = "daaMintStart")]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde_as(as = "Option<DisplayFromStr>")]
    pub daa_mint_start: Option<u64>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde_as(as = "Option<DisplayFromStr>")]
    pub premint: Option<u64>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde_as(as = "Option<DisplayFromStr>")]
    pub to: Option<String>,
}

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpMint {
    #[serde(rename = "p")]
    pub protocol: String,
    pub op: Op,
    pub tick: String,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<String>,
}

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpDiscount {
    #[serde(rename = "p")]
    pub protocol: String,
    pub op: Op,
    pub tick: String,
    pub to: String,
    #[serde_as(as = "DisplayFromStr")]
    #[serde(rename = "discountFee")]
    pub fee: u64,
}

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpTransfer {
    #[serde(rename = "p")]
    pub protocol: String,
    pub op: Op,
    pub tick: String,
    #[serde(rename = "id")]
    #[serde_as(as = "DisplayFromStr")]
    pub tokenid: u64,
    pub to: String,
}


#[derive(
    Debug, Clone, Copy, Eq, PartialEq, Deserialize, Serialize,
)]
#[serde(rename_all = "lowercase")]
pub enum Op {
    Deploy,
    Mint,
    Transfer,
    Discount,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
#[serde(rename_all = "lowercase")]
pub enum Metadata {
    // ipfs cid (ipfs://...)
    #[serde(rename = "buri")]
    Remote(String),
    #[serde(rename = "metadata")]
    Local(LocalMetadata),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct LocalMetadata {
    pub name: String,
    pub description: String,
    // ipfs cid (ipfs://...)
    pub image: String,
    pub attributes: Option<Vec<Attribute>>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Attribute {
    #[serde(rename = "traitType")]
    pub trait_type: String, // The name/type of the trait (e.g. "Background", "Eyes", "Rarity")
    pub value: String, // The value of the trait (e.g. "Blue", "Gold", "Rare")
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(rename = "displayType")]
    display_type: Option<String>, // The display type hint (e.g. "date", "boost_percentage")
}

To serialize this struct you need the following crates:

[dependencies]
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.132"
serde_with = "3.8.1"

TypeScript Interfaces

UserOperation

The following UserOperation struct can be used to represent a user operation submitted to the KRC-721 indexer.

Note: only buri or metadata must be used.


// The following types representing BigInt are serialized as string 
type SOMPI = string;
type DAA = string;
type U64 = string;
// The following type represents Kaspa Address and is serialized as string
type Address = string;

type UserOperation = Deploy | Mint | Transfer | Discount;

interface Deploy {
    p: 'krc-721';
    op: 'deploy';
    tick: string; // ticker symbol 1..10 alphanumeric characters
    buri: string | undefined; // optional URI for metadata as ipfs cid (ipfs://...)
    metadata: Metadata | undefined; // optional inscribed metadata
    max: U64 | undefined; // optional max number of tokens available to mint
    royaltyTo: Address | undefined; // optional royalty recipient address
    royaltyFee: SOMPI | undefined; // optional royalty fee
    daaMintStart: DAA | undefined; // optional start time for DAA minting
    premint: U64 | undefined; // optional premint
}

interface Discount {
    p: 'krc-721';
    op: 'discount';
    tick: string; // ticker symbol 1..10 alphanumeric characters
    to: Address | undefined; // recipient address
}

interface Mint {
    p: 'krc-721';
    op: 'mint';
    tick: string; // ticker symbol 1..10 alphanumeric characters
    to: Address | undefined; // optional recipient address
}

interface Transfer {
    p: 'krc-721';
    op: 'transfer';
    tick: string; // ticker symbol 1..10 alphanumeric characters
    tokenid: string | undefined; // token id
    to: Address | undefined; // recipient address
}

interface Metadata {
    name: string;
    description: string;
    image: string; // ipfs cid (ipfs://...)
    attributes: Attribute[];
}

interface Attribute {
    traitType: string; // The name/type of the trait (e.g. "Background", "Eyes", "Rarity")
    value: string; // The value of the trait (e.g. "Blue", "Gold", "Rare")
    displayType?: string; // Optional display type hint (e.g. "date", "boost_percentage")
}