KSPR 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:

The mainnet deployment will be launched after completion of comprehensive indexer testing.

Wallets

KSPR Browser Extension wallet is a fully-featured wallet for the Kaspa network that supports KRC-20 and KRC-721 standards.

REST API Specifications

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 to 400.

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

Offsets

Resource listing endpoints are paginated if the number of records is more than the user specified limit or a maximum default limit of 50 records.

If the number of records exceeds the limit, the next page offset will be returned in the response specified within the next field.

To obtain the next page, the user must provide the next value in the offset parameter of the query string.

The following example aggregates all pages until the next is undefined.

fetch page
next = page.next

while next
    fetch page?offset=next
    next = page.next

Direction

The direction of the record iteration can be specified in the query string using the direction parameter. The default direction is forward.

The following directions are supported:

Specifying a backward direction will return records in reverse iteration order.

Limits

The number of records to return can be specified in the query string using the limit parameter. The maximum (and the default) limit is 50. Specifying a limit greater than 50 will be ignored returning maximum 50 records.

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

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}

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}

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

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.

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 premint receiver address
}

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

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 KSPR 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 KSPR 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; // The display type hint (e.g. "date", "boost_percentage")
}