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.

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 | "13000000000000" | | /ops | Score | Numeric string | "13000000000000" | | /deployments | Score | Numeric string | "13000000000000" | | /history/{tick}/{id} | Score | Numeric string | "13000000000000" | | /owners/{tick} | TokenId | Numeric string | "51" | | /address/{address}/{tick} | TokenId | Numeric string | "51" | | /address/{address} | TickTokenOffset | String "TICK-tokenId" | "FOO-123" |

⚠️ 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) break;  // No more pages
  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: Not checking if next is null ✅ Correct: Check if (!data.next) break;

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

REST endpoints

Indexer Status

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

Response:

{
    "message": "text",
    "result": {
        "version": "string",
        "network": "string",
        "isNodeConnected": true,
        "isNodeSynced": true,
        "isIndexerSynced": true,
        "lastKnownBlockHash": "string",
        "daaScore": "uint64",
        "powFeesTotal": "uint64",
        "royaltyFeesTotal": "uint64",
        "tokenDeploymentsTotal": "uint64",
        "tokenMintsTotal": "uint64",
        "tokenTransfersTotal": "uint64"
    }
}

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": "text",
    "prev": "text",
    "next": "text",
    "result": [
        {
            "deployer": "kaspatest:qqqqqqqqqqqqqqq",
            "buri": "ipfs://QOWmd",
            "max": "250",
            "daaMintStart": "0",
            "premint": "6",
            "tick": "FOO",
            "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
            "mtsAdd": "1000000000",
            "minted": "6",
            "opScoreMod": "1000000000",
            "state": "deployed",
            "mtsMod": "1000000000",
            "opScoreAdd": "1000000000"
        }
    ],
    "next": 13000000000000
}

Get Collection Details

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

Response:

{
    "message": "text",
    "result":
    {
        "deployer": "kaspatest:qqqqqqqqqqqqqqqqq",
        "royaltyTo": "kaspatest:qqqqqqqqqqqqqqqqqqq",
        "buri": "ipfs://dz1...",
        "max": "800",
        "royaltyFee": "2500000000",
        "daaMintStart": "0",
        "premint": "10",
        "tick": "FOO",
        "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
        "mtsAdd": "1000000000",
        "minted": "556",
        "opScoreMod": "1000000000",
        "state": "deployed",
        "mtsMod": "1000000000",
        "opScoreAdd": "1000000000"
    }
}

Tokens

Get Token Details

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

Response:

{
    "message": "text",
    "result":
    {
        "tick": "FOO",
        "tokenId": "123",
        "owner": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
        "buri": "ipfs://..."
    }
}

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": "text",
    "result": [
        {
            "tick": "FOO",
            "tokenId": "123",
            "owner": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
            "opScoreMod": "1000000000"
        }
    ],
    "next": 51
}

Address Holdings

Get Address NFT List

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

Query Parameters:

⚠️ CRITICAL: This endpoint uses TickTokenOffset format ("FOO-123"), NOT a numeric offset!

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

Response:

{
    "message": "text",
    "result": [
        {
            "tick": "FOO",
            "tokenId": "381",
            "buri": "ipfs://..."
        },
        {
            "tick": "FOO",
            "tokenId": "382",
            "buri": "ipfs://..."
        },
        {
            "tick": "FOO",
            "tokenId": "31010",
            "buri": "ipfs://..."
        }
    ],
    "next":"FOO-123"
}

Get Address Collection Holdings

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

Response:

{
    "message": "text",
    "result":
    {
        "tick": "FOO",
        "tokenId": "381",
        "opScoreMod": "79993666"
    }
}

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:

{
    "message": "text",
    "result": [
        {
            "p": "krc-721",
            "deployer": "kaspatest:kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
            "op": "deploy",
            "tick": "FOO",
            "opData": {
                "buri": "ipfs://...",
                "max": "456"
            },
            "opScore": "123",
            "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
            "mtsAdd": "00000000000000", 
            "opError": "InsufficientFee",
            "feeRev": "123"
        },
        {
            "p": "krc-721",
            "deployer": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
            "op": "mint",
            "tick": "FOO",
            "opData": {
                "tokenId": "1234",
                "to": "0000"
            },
            "opScore": "123",
            "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
            "opError": "InsufficientFee",
            "mtsAdd": "00000000000000",
            "feeRev": "123"
        },    
        {
            "p": "krc-721",
            "deployer": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
            "op": "transfer",
            "tick": "FOO",
            "opData": {
                "tokenId": "1234",
                "to": "0000"
            },
            "opScore": "123",      
            "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
            "opError": "InsufficientFee",
            "mtsAdd": "00000000000000",
            "feeRev": "123"
        }
    ],
    "next": 13000000000000
}

Get Operation Details by Score

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

Response:

{
    "message": "text",
    "result":
    {
        "p": "krc-721",
        "deployer": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
        "royalty_to": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
        "tick": "FOO",
        "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
        "mtsAdd": "00000000000000",
        "op": "deploy",
        "opData": {
            "buri": "ipfs://",
            "max": "10000",
            "royaltyFee": "12300000",
            "daaMintStart": "0",
            "premint": "123"
        },
        "opError": "InsufficientFee",
        "opScore": "123",
        "feeRev": "123"
    }
}

Get Operation Details by Transaction ID

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

Response:

{
    "message": "text",
    "result":
    {
        "p": "krc-721",
        "deployer": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
        "to": "kaspatest:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
        "tick": "FOO",
        "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000",
        "mtsAdd": "0000000000000",
        "op": "deploy",
        "opData": {
            "buri": "ipfs://",
            "max": "1230",
            "daaMintStart": "0"
        },
        "opError": "InsufficientFee",
        "opScore": "123",
        "feeRev": "123"
    }
}

Get Royalty Fees for a given address and tick

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

Response:

{
    "message": "text",
    "result": "1000000000"
}

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:

{
    "message": "text",
    "result": "rejection reason"
}

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:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
      "opScoreMod": "2000000000",
      "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000"
    },
    {
      "owner": "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
      "opScoreMod": "1002200000",
      "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000"
    },
    {
      "owner": "kaspa:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
      "opScoreMod": "1000000001",
      "txIdRev": "0000000000000000000000000000000000000000000000000000000000000000"
    }
  ],
  "next": "100000000"
}

Get Available Token ID Ranges

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

Get available token ID ranges for minting in a collection. Response:

{
    "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": ""
}

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")
}