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
- Security budget-driven transactions (fixed PoW fees)
- Optional pre-minting during deployment
- Optional discounted minting (reduced royalty fees per address)
- Optional royalty fees
- Optional DAA start time for minting
- Metadata can be either inscribed or specified as an external URL
- Full randomization of tokens during minting
- Creator-defined number of tokens per collection.
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:
message - A human-readable message describing the result of the request.
result (optional) - The result of the request.
next (optional) - The offset of the next page. Omitted entirely (not set to null) when there are no more pages.
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:
- Use the
next value from the response as the offset parameter for the next request
- Offset formats vary by endpoint (numeric Score, numeric TokenId, or string TickTokenOffset)
- When
next is absent from the response, there are no more pages
- Never manually increment offsets - always use the
next value provided
Query Parameters
offset (optional): Cursor for pagination. Use the next value from the previous response. Format depends on endpoint.
limit (optional): Number of records per page. Default: 50, Maximum: 50.
direction (optional): Iteration direction. Values: forward (default) or backward/back.
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
forward (default): Iterate from first to last record (chronological order)
backward or back: Iterate from last to first record (reverse chronological order)
Use direction=backward to get the latest items first without pagination.
Limits
- Default: 50 records per page
- Maximum: 50 records (values greater than 50 are ignored)
- Minimum: 1 record
Common Mistakes
-
Wrong: Manually incrementing offset (offset += 50)
Correct: Use next value from response
-
Wrong: Using numeric offset for /address/{address} endpoint
Correct: Use TickTokenOffset format "TICK-tokenId"
-
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:
offset (optional): Pagination offset (Score format - numeric string)
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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:
offset (optional): Pagination offset (TokenId format - numeric)
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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:
offset (optional): Pagination offset (TickTokenOffset format - string "TICK-tokenId")
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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:
offset (optional): Pagination offset (TokenId format - numeric)
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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:
offset (optional): Pagination offset (Score format - numeric string)
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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:
offset (optional): Pagination offset (Score format - numeric string)
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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:
offset (optional): Pagination offset (Score format - numeric string)
limit (optional): Number of records (1-50, default: 50)
direction (optional): forward (default) or backward
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
- Request a page: Make a request to a paginated endpoint (optionally with
offset, limit, and direction parameters)
- Check for more data: Look for the
next field in the response
- Fetch next page: If
next is present, use its value as the offset parameter for the next request
- 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:
next is null or undefined when there are no more pages
next contains the offset value to use for the next request
- The offset format varies by endpoint (see Offset Types below)
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:
/api/v1/krc721/{network}/nfts - Collections list
/api/v1/krc721/{network}/ops - Operations list
/api/v1/krc721/{network}/deployments - Deployments list
/api/v1/krc721/{network}/history/{tick}/{id} - Ownership history
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:
/api/v1/krc721/{network}/owners/{tick} - Token owners list
/api/v1/krc721/{network}/address/{address}/{tick} - Address collection holdings
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:
/api/v1/krc721/{network}/address/{address} - All NFTs for an address
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.
- Type: String (format depends on endpoint - see Offset Types above)
- Default:
forward direction: Start from beginning (offset = 0 or minimum)
backward direction: Start from end (offset = maximum)
- Example:
?offset=13000000000000 or ?offset=FOO-123
limit (optional)
Number of records to return per page.
- Type: Integer (passed as string in query:
?limit=20)
- Default: 50
- Maximum: 50 (values greater than 50 are automatically capped at 50)
- Example:
?limit=20
direction (optional)
Direction of iteration.
- Values:
forward (default) or backward (or back)
- Forward: Iterate from first to last record (chronological order)
- Backward: Iterate from last to first record (reverse chronological order - newest first)
- Example:
?direction=backward or ?direction=back
Note: These parameters work together. You can combine them:
?limit=10&direction=backward - Get latest 10 items
?offset=123&limit=20&direction=forward - Get next 20 items starting from offset 123
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)
- Offset Type: Score (numeric string)
- Default Direction: Forward (oldest first)
- Use Case: Browse all collections chronologically
Operations List (/ops)
- Offset Type: Score (numeric string)
- Default Direction: Forward (oldest first)
- Use Case: Monitor all operations
Deployments List (/deployments)
- Offset Type: Score (numeric string)
- Default Direction: Forward (oldest first)
- Use Case: Track new collection deployments
- Tip: Use
direction=backward to get latest deployments first
Address NFT List (/address/{address})
- Offset Type: TickTokenOffset (string format: "TICK-tokenId")
- Default Direction: Forward
- Use Case: Get all NFTs owned by an address
- Important: Offset format is
"FOO-123", not numeric!
Token Owners (/owners/{tick})
- Offset Type: TokenId (numeric string)
- Default Direction: Forward
- Use Case: List all tokens and their owners in a collection
Ownership History (/history/{tick}/{id})
- Offset Type: Score (numeric string)
- Default Direction: Forward
- Use Case: Track ownership changes for a specific token
Performance Considerations
- Limit Size: Use appropriate limits (default 50 is usually good)
- Caching: Consider caching paginated results
- Parallel Requests: Don't make parallel requests with the same offset
- Rate Limiting: Be aware of rate limits (if configured)
Testing Pagination
Use the sandbox (/sandbox) to test pagination:
- Navigate to a paginated endpoint
- Enter query parameters:
limit=10&direction=forward
- Execute the request
- Copy the
next value
- Use it as
offset in the next request
Summary
- Always use the
next value from the response as the offset for the next request
- Offset formats vary: Score (numeric), TokenId (numeric), or TickTokenOffset (string "TICK-tokenId")
- Check endpoint documentation to know which offset type to expect
next is null when there are no more pages
- Don't manually increment offsets - use the
next value provided
- Use
direction=backward to get latest items first
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):
- Unique identifier for operations in chronological order
- Used for pagination in score-based endpoints
- Format: Unsigned 64-bit integer (string representation)
- Higher values = more recent operations
DAA Score (daaScore, daaMintStart):
- Kaspa's Difficulty Adjustment Algorithm score
- Represents blockchain time/progress
- Format: Unsigned 64-bit integer (string representation)
- Used to determine when minting can start (
daaMintStart)
Timestamp Fields
mtsAdd (Milliseconds Timestamp Added):
- Timestamp when item was added to the indexer
- Format: Milliseconds since Unix epoch (uint64 as string)
- Example:
"1712808987852" = May 10, 2024
mtsMod (Milliseconds Timestamp Modified):
- Timestamp when item was last modified
- Format: Milliseconds since Unix epoch (uint64 as string)
Transaction ID Fields
txIdRev (Transaction ID Reversed):
- Transaction ID with bytes in reverse order
- Format: 64-character hexadecimal string
- Example:
"0000000000000000000000000000000000000000000000000000000000000000"
Why reversed? Internal database optimization for lexicographic ordering.
Amount Fields
All amounts are in SOMPI (10^-8 KAS):
- Format: Unsigned 64-bit integer (string representation)
- To convert to KAS:
sompi / 100000000
- Examples:
"100000000" = 1 KAS
"10000000000" = 100 KAS
"1000000000" = 10 KAS
State Fields
state (Collection State):
- Possible values:
"deployed"
- Indicates collection deployment status
Collection State Values
"deployed": Collection is active and can accept mints/transfers
Field Naming Conventions
- camelCase: Most fields use camelCase (e.g.,
tokenId, opScoreMod)
- Abbreviations: Common abbreviations:
op = operation
mts = milliseconds timestamp
daa = Difficulty Adjustment Algorithm
rev = reversed
mod = modified
add = added
buri = base URI
Data Type Notes
- All numeric fields are returned as strings to avoid JavaScript number precision issues
- All addresses are Kaspa addresses with network prefix (e.g.,
kaspa:... or kaspatest:...)
- All IPFS URIs must start with
ipfs:// prefix
- All transaction IDs are 64-character hexadecimal strings (may be reversed)
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:
- HTTP Status:
404
- Content-Type:
text/plain
- Body:
"not found"
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:
- HTTP Status:
400
- Content-Type:
application/json
- Body: JSON with error details
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:
- HTTP Status:
429
- Headers: May include
Retry-After header
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:
- HTTP Status:
500
- Content-Type:
text/plain or application/json
- Body: Error message
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
- Always check HTTP status codes before parsing JSON
- Check
message === 'success' even when status is 200
- Handle 404 gracefully - it's a normal case (resource doesn't exist)
- Implement retry logic for network errors and 5xx errors
- Use exponential backoff for rate limiting
- Log errors with context for debugging
- Validate inputs before making requests
- Set timeouts on all requests
- Handle JSON parsing errors separately from network errors
- 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:
- Wrong network specified in URL
- Collection tick is case-sensitive
- 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:
- Using wrong offset format
- Manually incrementing offset instead of using
next
- Offset format mismatch (using numeric for TickTokenOffset endpoint)
Solution: See PAGINATION.md for correct pagination implementation.
Problem: CORS errors in browser
Possible causes:
- Making requests from different origin
- Server CORS not configured
Solution: CORS is enabled by default. If issues persist, check server configuration.
Problem: Timeout errors
Possible causes:
- Network connectivity issues
- Server overloaded
- Request taking too long (default timeout: 10 seconds)
Solution: Implement retry logic with exponential backoff.
See Also
KRC-721 Standard Specifications
Data types
- All amounts provided by and supplied to the indexer are in
SOMPI (10^-8 KAS)
Restrictions
- Only URLs containing
ipfs:// prefix will be accepted as a valid buri or metadata.image fields.
Security budget (PoW fees)
For a sustainable ecosystem, the security budget for Kaspa miners is ensured through the following mandatory operation fees:
- Deploy = minimum
1,000 KAS reveal transaction fees required
- Pre-mint (part of Deploy) = minimum
10 KAS per token (added to the Deployment fee)
- Mint = minimum
10 KAS reveal transaction fees required
- Discount = no fee (standard transaction fee)
- Transfer = no fee (standard transaction fee)
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"
}
- Metadata shall be hosted at URI
{metadata} or inscribed (the size on chain is limited).
- The metadata structure requires
name, description and image fields.
- Image must be either a valid URI or data URI with valid mime type.
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
- Tick must be 1-10 alphanumeric characters
- Tick must be unique (case-insensitive)
- Max supply must be specified as a positive numerical value
- Metadata must be either:
- A valid URI string declared as
buri (external metadata)
- A valid
metadata object (inscribed metadata)
- Metadata (inscribed) must include name, description, and image fields
- Metadata (inscribed) attributes are optional
- RoyaltyFee is optional and must be between >0 and 10,000,000 KAS if specified
- RoyaltyOwner must be a valid Kaspa address if specified
- Else defaults to the deployer address
- Security budget: reveal transaction fees >= deploy operation miner fees.
- Premint cannot be greater than maximum amount of tokens.
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
- Collection must exist and have unminted items available
- Total minted cannot exceed collection max supply
- Optional recipient address must be valid if specified
- Defaults to mint transaction sender if not specified
- Must include royalties payment to the royalties beneficiary in the first output of mint reveal transaction when collection has royalties enabled
- Security budget: reveal transaction fees >= mint operation miner fees.
- Must happen after mintDaaScore if specified during deployment.
Transfer
Transfers ownership of a token.
{
"p": "krc-721",
"op": "transfer",
"tick": "string",
"id": "uint64",
"to": "string" // Kaspa recipient address
}
Validation Rules
- Collection and token must exist
- Sender must be the owner of token #{tokenid}
- Recipient address must be valid
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
- Collection and token must exist
- Sender must be the deployer of the collection
- Recipient address must be valid
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")
}