Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

DenshokanClient

Source: denshokan-sdk

The DenshokanClient is the core class for interacting with EGS data. It provides methods for querying games, tokens, players, and activity through REST API and Starknet RPC.

Configuration

import { DenshokanClient } from "@provable-games/denshokan-sdk";
 
const client = new DenshokanClient({
  chain: "mainnet",           // "mainnet" | "sepolia"
  apiUrl: "https://...",      // Override API URL
  wsUrl: "wss://...",         // Override WebSocket URL
  rpcUrl: "https://...",      // Override RPC URL
  primarySource: "api",       // "api" | "rpc" (default: "api")
  viewerAddress: "0x...",     // Viewer contract for batch filter queries
  denshokanAddress: "0x...",  // Denshokan ERC721 contract address
  registryAddress: "0x...",   // Minigame registry contract address
  fetch: {
    timeout: 10000,           // Request timeout (ms)
    maxRetries: 3,            // Max retry attempts
    baseBackoff: 1000,        // Base backoff delay (ms)
    maxBackoff: 30000,        // Max backoff delay (ms)
    tokenUriConcurrency: 0,   // Max concurrent URI fetches (0 = unlimited)
  },
  ws: {
    maxReconnectAttempts: 10, // Max reconnect attempts
    reconnectBaseDelay: 1000, // Base reconnect delay (ms)
  },
  health: {
    initialCheckDelay: 1000,  // ms before first health check
    checkInterval: 30000,     // ms between health checks
    checkTimeout: 5000,       // ms timeout for health check
  },
});

See DenshokanClientConfig for the full configuration type.

Default Endpoints

When you specify chain: "mainnet" or chain: "sepolia", the SDK uses these defaults. All URLs can be overridden via apiUrl, wsUrl, and rpcUrl in the config.

Mainnet

ServiceURL
APIhttps://denshokan-api-production.up.railway.app
WebSocketwss://denshokan-api-production.up.railway.app/ws
RPChttps://api.cartridge.gg/x/starknet/mainnet
Contract addresses:
ContractAddress
Denshokan0x0029ffae8b0c4626e06395a947800bc89e76422107f6adff8937a6e9a1e01f28
Registry0x05b4a2ed39dfb28a33c2dd73cbedf02091a31dccb9ed4ed19201e3c255865851
Viewer0x01825fa210dc2abd02fa03d4eb37dabf1d6b69e9c4cd471ee402fa0fcc78611b

Sepolia

ServiceURL
APIhttps://denshokan-api-sepolia.up.railway.app
WebSocketwss://denshokan-api-sepolia.up.railway.app/ws
RPChttps://api.cartridge.gg/x/starknet/sepolia
Contract addresses:
ContractAddress
Denshokan0x0142712722e62a38f9c40fcc904610e1a14c70125876ecaaf25d803556734467
Registry0x040f1ed9880611bb7273bf51fd67123ebbba04c282036e2f81314061f6f9b1a1
Viewer0x025d92f18c6c1ed2114774adf68249a95fc468d9381ab33fa4b9ccfff7cf5f9f

REST API Routes

All REST endpoints are relative to the API base URL above.

MethodPathDescription
GET/healthHealth check
GET/gamesList games (paginated)
GET/games/{gameAddress}Get a single game
GET/games/{gameAddress}/statsGame statistics
GET/games/{gameAddress}/settingsGame settings
GET/games/{gameAddress}/settings/{settingsId}Single game setting
GET/games/{gameAddress}/objectivesGame objectives
GET/games/{gameAddress}/objectives/{objectiveId}Single game objective
GET/settingsGlobal settings
GET/objectivesGlobal objectives
GET/tokensList tokens (filterable)
GET/tokens/{tokenId}Get a single token
GET/tokens/{tokenId}/scoresToken score history
GET/players/{address}/tokensPlayer's tokens
GET/players/{address}/statsPlayer statistics
GET/mintersList minters
GET/minters/{minterId}Get a single minter
GET/activityActivity events (filterable)
GET/activity/statsActivity statistics

WebSocket Channels

Connect to the WebSocket URL and subscribe to real-time events:

ChannelDescription
tokensNew token mints and updates
scoresScore changes
game_overGame completion events
mintsMint events

Games

// List games with pagination
const games = await client.getGames({ limit: 20, offset: 0 });
// Returns: PaginatedResult<Game>
 
// Get a single game (API with RPC fallback)
const game = await client.getGame("0x1234...");
// Returns: Game
 
// Get game statistics
const stats = await client.getGameStats("0x1234...");
// Returns: GameStats

Tokens

// List tokens with filtering
const tokens = await client.getTokens({
  gameAddress: "0x1234...",
  owner: "0x5678...",
  settingsId: 2,
  gameOver: true,
  playable: false,
  soulbound: false,
  includeUri: true,    // Fetch token URIs via batch RPC
  limit: 50,
  offset: 0,
});
// Returns: PaginatedResult<Token>
 
// Get a single token (API with RPC fallback)
const token = await client.getToken("0xabc...");
// Returns: Token
 
// Get score history
const scores = await client.getTokenScores("0xabc...", 10);
// Returns: TokenScoreEntry[]

Token Filter Parameters

ParameterTypeDescription
gameAddressstringFilter by game contract address
gameIdnumberFilter by game ID
ownerstringFilter by token owner
settingsIdnumberFilter by settings configuration
objectiveIdnumberFilter by objective
minterAddressstringFilter by who minted the token
soulboundbooleanFilter by soulbound status
playablebooleanFilter by playability
gameOverbooleanFilter by game completion
mintedAfternumberFilter by mint timestamp (after)
mintedBeforenumberFilter by mint timestamp (before)
includeUribooleanFetch token URIs via batch RPC
limitnumberPage size
offsetnumberPage offset

See TokensFilterParams for the full type.

Players

// Get player's tokens
const tokens = await client.getPlayerTokens("0x5678...", {
  gameAddress: "0x1234...",  // Optional: filter by game
  includeUri: true,          // Optional: fetch token URIs
});
// Returns: PaginatedResult<Token>
 
// Get player statistics
const stats = await client.getPlayerStats("0x5678...");
// Returns: PlayerStats

Minters

// List minters with pagination
const minters = await client.getMinters({ limit: 20, offset: 0 });
// Returns: PaginatedResult<Minter>
 
// Get a single minter
const minter = await client.getMinter("minter-id");
// Returns: Minter

See Minter for the type definition.

Activity

// List activity events
const activity = await client.getActivity({
  type: "score_update",  // Optional: filter by event type
  limit: 50,
  offset: 0,
});
// Returns: PaginatedResult<ActivityEvent>
 
// Get aggregate activity stats
const stats = await client.getActivityStats(1); // Optional: gameId filter
// Returns: ActivityStats

See ActivityEvent and ActivityStats for the type definitions.

Settings & Objectives

// Get settings for a game
const settings = await client.getSettings({
  gameAddress: "0x1234...",
  limit: 10,
  offset: 0,
});
// Returns: PaginatedResult<GameSettingDetails>
 
// Get a specific setting
const setting = await client.getSetting(2, "0x1234...");
// Returns: GameSettingDetails
 
// Get objectives for a game
const objectives = await client.getObjectives({
  gameAddress: "0x1234...",
  settingsId: 1,  // Optional: filter by settings
});
// Returns: PaginatedResult<GameObjectiveDetails>
 
// Get a specific objective
const objective = await client.getObjective(1, "0x1234...");
// Returns: GameObjectiveDetails

RPC Methods

Direct contract reads when you need on-chain truth.

ERC721

const balance = await client.balanceOf("0x5678...");   // bigint
const owner = await client.ownerOf("0xabc...");        // string
const uri = await client.tokenUri("0xabc...");         // string
const name = await client.name();                       // string
const sym = await client.symbol();                      // string
const supply = await client.totalSupply();              // bigint
 
// Batch URI fetch
const uris = await client.tokenUriBatch(["0xa..", "0xb.."]);
// Returns: string[]
 
// ERC-2981 royalty info
const royalty = await client.royaltyInfo("0xabc..", salePrice);
// Returns: RoyaltyInfo { receiver, amount }

ERC721 Enumerable

// Get token ID by global index
const tokenId = await client.tokenByIndex(0n);
 
// Get token ID by owner index
const tokenId = await client.tokenOfOwnerByIndex("0x5678...", 0n);
 
// Enumerate all token IDs (paginated)
const ids = await client.enumerateTokenIds({ limit: 100, offset: 0 });
 
// Enumerate token IDs owned by address
const ids = await client.enumerateTokenIdsByOwner("0x5678...", {
  limit: 100,
  offset: 0,
});

Token Metadata

Single and batch accessors for on-chain token metadata. Each method has a batch variant that accepts an array of token IDs.

MethodReturnsBatch Variant
tokenMetadata(tokenId)TokenMetadatatokenMetadataBatch(tokenIds)
tokenMutableState(tokenId)TokenMutableStatetokenMutableStateBatch(tokenIds)
isPlayable(tokenId)booleanisPlayableBatch(tokenIds)
settingsId(tokenId)numbersettingsIdBatch(tokenIds)
playerName(tokenId)stringplayerNameBatch(tokenIds)
objectiveId(tokenId)numberobjectiveIdBatch(tokenIds)
mintedBy(tokenId)stringmintedByBatch(tokenIds)
isSoulbound(tokenId)booleanisSoulboundBatch(tokenIds)
rendererAddress(tokenId)stringrendererAddressBatch(tokenIds)
tokenGameAddress(tokenId)stringtokenGameAddressBatch(tokenIds)
// Single
const metadata = await client.tokenMetadata("0xabc...");
const playable = await client.isPlayable("0xabc...");
const name = await client.playerName("0xabc...");
 
// Batch
const metadatas = await client.tokenMetadataBatch(["0xa..", "0xb.."]);
const names = await client.playerNameBatch(["0xa..", "0xb.."]);

Game Contract Reads

Read game-specific state directly from on-chain contracts.

Score & Game Over:
const score = await client.score("0xabc...", "0x1234...");   // bigint
const over = await client.gameOver("0xabc...", "0x1234..."); // boolean
 
// Batch
const scores = await client.scoreBatch(["0xa..", "0xb.."], "0x1234...");
const overs = await client.gameOverBatch(["0xa..", "0xb.."], "0x1234...");
Token Details (game-specific):
const name = await client.tokenName("0xabc..", "0x1234..");
const desc = await client.tokenDescription("0xabc..", "0x1234..");
const details = await client.gameDetails("0xabc..", "0x1234..");
// Returns: GameDetail[] — array of { key, value } pairs
 
// Batch variants
const names = await client.tokenNameBatch(["0xa..", "0xb.."], "0x1234..");
const descs = await client.tokenDescriptionBatch(["0xa..", "0xb.."], "0x1234..");
const allDetails = await client.gameDetailsBatch(["0xa..", "0xb.."], "0x1234..");
Objectives (on-chain):
const count = await client.objectivesCount("0x1234..");             // number
const exists = await client.objectiveExists(1, "0x1234..");         // boolean
const completed = await client.completedObjective(
  "0xabc..", 1, "0x1234.."
);  // boolean
const details = await client.objectivesDetails(1, "0x1234..");      // GameObjectiveDetails
 
// Batch variants
const existsAll = await client.objectiveExistsBatch([1, 2, 3], "0x1234..");
const completedAll = await client.completedObjectiveBatch(
  ["0xa..", "0xb.."], 1, "0x1234.."
);
const detailsAll = await client.objectivesDetailsBatch([1, 2, 3], "0x1234..");
Settings (on-chain):
const count = await client.settingsCount("0x1234..");              // number
const exists = await client.settingsExists(1, "0x1234..");         // boolean
const details = await client.settingsDetails(1, "0x1234..");       // GameSettingDetails
 
// Batch variants
const existsAll = await client.settingsExistsBatch([1, 2, 3], "0x1234..");
const detailsAll = await client.settingsDetailsBatch([1, 2, 3], "0x1234..");

Registry RPC

// Get full on-chain game metadata from registry
const meta = await client.gameMetadata(1);       // GameMetadata
const addr = await client.gameAddress(1);        // string

Write Operations

Write methods return transaction call data for use with starknet.js account.execute().

// Mint a new game token
const calls = client.mint({
  gameId: 1,
  settingsId: 2,
  objectiveId: 0,
  playerName: "Player1",
  skillsAddress: undefined,   // Optional: AI agent skills provider
  to: playerAddress,
  soulbound: false,
});
 
// Batch mint (auto-assigns salts)
const calls = client.mintBatch([
  { gameId: 1, settingsId: 1, objectiveId: 0, playerName: "P1", to: addr, soulbound: false },
  { gameId: 1, settingsId: 2, objectiveId: 0, playerName: "P2", to: addr, soulbound: false },
]);
 
// Update game state (sync score/game_over from game contract)
const calls = client.updateGame(tokenId);
const calls = client.updateGameBatch([tokenId1, tokenId2]);
 
// Update player name
const calls = client.updatePlayerName(tokenId, "NewName");
const calls = client.updatePlayerNameBatch([
  { tokenId: "0xa..", name: "Name1" },
  { tokenId: "0xb..", name: "Name2" },
]);

Executing Transactions

Write methods return Call[] arrays. Use starknet.js to execute them:

import { Account, RpcProvider } from "starknet";
 
const provider = new RpcProvider({ nodeUrl: "https://..." });
const account = new Account(provider, accountAddress, privateKey);
 
// Execute a mint
const mintCalls = client.mint({
  gameId: 1,
  settingsId: 1,
  objectiveId: 0,
  playerName: "MyPlayer",
  to: account.address,
  soulbound: false,
});
const { transaction_hash } = await account.execute(mintCalls);
await provider.waitForTransaction(transaction_hash);

With Cartridge Controller:

import { useAccount } from "@starknet-react/core";
 
function MintButton({ client }) {
  const { account } = useAccount();
 
  const handleMint = async () => {
    const calls = client.mint({
      gameId: 1,
      settingsId: 1,
      objectiveId: 0,
      playerName: "Player",
      to: account.address,
      soulbound: false,
    });
    await account.execute(calls);
  };
 
  return <button onClick={handleMint}>Mint Token</button>;
}

See MintParams for the full parameter type.

Utilities

Decode Token ID

Extract all 14 packed fields without any RPC call:

const decoded = client.decodeTokenId("0x123...");
// Returns: DecodedTokenId {
//   tokenId: bigint,
//   gameId: number,
//   mintedBy: bigint,
//   settingsId: number,
//   mintedAt: Date,
//   startDelay: number,
//   endDelay: number,
//   objectiveId: number,
//   soulbound: boolean,
//   hasContext: boolean,
//   paymaster: boolean,
//   txHash: number,
//   salt: number,
//   metadata: number,
// }

The standalone decodeCoreToken() function returns a CoreToken without needing a client instance:

import { decodeCoreToken } from "@provable-games/denshokan-sdk";
 
const core = decodeCoreToken("0x123...");
// Returns: CoreToken — no RPC, no client needed

MintSaltCounter

Manages auto-incrementing 10-bit salt values (0–1023) for token minting:

import { MintSaltCounter } from "@provable-games/denshokan-sdk";
 
const counter = new MintSaltCounter();    // starts at 0
const salt1 = counter.next();             // 0
const salt2 = counter.next();             // 1
const peeked = counter.peek();            // 2 (without advancing)
counter.reset();                          // back to 0

assignSalts

Assigns auto-incrementing salts to mint parameter arrays:

import { assignSalts } from "@provable-games/denshokan-sdk";
 
const params = [
  { gameId: 1, settingsId: 1, objectiveId: 0, playerName: "P1", to: addr, soulbound: false },
  { gameId: 1, settingsId: 2, objectiveId: 0, playerName: "P2", to: addr, soulbound: false },
];
 
const withSalts = assignSalts(params);
// withSalts[0].salt === 0
// withSalts[1].salt === 1

Address Utilities

import { normalizeAddress, toHexTokenId } from "@provable-games/denshokan-sdk";
 
// Normalize to 0x-prefixed, 64-char hex (strips leading zeros, pads)
normalizeAddress("0x00123");
// "0x0000000000000000000000000000000000000000000000000000000000000123"
 
// Convert bigint/hex/decimal to 0x-prefixed hex (no padding)
toHexTokenId(255n);
// "0xff"

Connection Status

// Check WebSocket connection
const connected = client.wsConnected; // boolean
 
// Listen for connection changes
const unsub = client.onWsConnectionChange((connected) => {
  console.log("WebSocket:", connected ? "up" : "down");
});
 
// Later: stop listening
unsub();

Health Monitoring

The SDK performs background health checks to detect API or RPC availability and automatically switch data sources.

// ConnectionStatus modes
type ConnectionMode = "api" | "rpc-fallback" | "offline";
StatusMeaning
apiAPI is reachable, using primary data source
rpc-fallbackAPI is unavailable, using RPC as fallback
offlineBoth API and RPC are unreachable

Health checks run at 30-second intervals (configurable via health.checkInterval). When the API goes down, the SDK auto-switches to RPC. When the API recovers, it switches back.

Error Handling

import {
  DenshokanError,
  ApiError,
  RpcError,
  TokenNotFoundError,
  DataSourceError,
} from "@provable-games/denshokan-sdk";
 
try {
  const token = await client.getToken(id);
} catch (error) {
  if (error instanceof TokenNotFoundError) {
    console.log(`Token ${error.tokenId} not found`);
  } else if (error instanceof ApiError) {
    console.log(`API error: ${error.statusCode}`);
  } else if (error instanceof DataSourceError) {
    console.log("Both API and RPC failed");
  }
}

Error Hierarchy

ErrorDescription
DenshokanErrorBase error class
ApiErrorREST API returned an error (has statusCode)
RpcErrorStarknet RPC call failed
RateLimitErrorRate limit exceeded (has retryAfter)
TimeoutErrorRequest timed out
AbortErrorRequest was aborted
TokenNotFoundErrorToken doesn't exist
GameNotFoundErrorGame doesn't exist
InvalidChainErrorInvalid chain specified
DataSourceErrorBoth primary and fallback sources failed

See Error Types for the full class definitions.

Cleanup

// Disconnect WebSocket and cleanup
client.disconnect();