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
| Service | URL |
|---|---|
| API | https://denshokan-api-production.up.railway.app |
| WebSocket | wss://denshokan-api-production.up.railway.app/ws |
| RPC | https://api.cartridge.gg/x/starknet/mainnet |
| Contract | Address |
|---|---|
| Denshokan | 0x0029ffae8b0c4626e06395a947800bc89e76422107f6adff8937a6e9a1e01f28 |
| Registry | 0x05b4a2ed39dfb28a33c2dd73cbedf02091a31dccb9ed4ed19201e3c255865851 |
| Viewer | 0x01825fa210dc2abd02fa03d4eb37dabf1d6b69e9c4cd471ee402fa0fcc78611b |
Sepolia
| Service | URL |
|---|---|
| API | https://denshokan-api-sepolia.up.railway.app |
| WebSocket | wss://denshokan-api-sepolia.up.railway.app/ws |
| RPC | https://api.cartridge.gg/x/starknet/sepolia |
| Contract | Address |
|---|---|
| Denshokan | 0x0142712722e62a38f9c40fcc904610e1a14c70125876ecaaf25d803556734467 |
| Registry | 0x040f1ed9880611bb7273bf51fd67123ebbba04c282036e2f81314061f6f9b1a1 |
| Viewer | 0x025d92f18c6c1ed2114774adf68249a95fc468d9381ab33fa4b9ccfff7cf5f9f |
REST API Routes
All REST endpoints are relative to the API base URL above.
| Method | Path | Description |
|---|---|---|
| GET | /health | Health check |
| GET | /games | List games (paginated) |
| GET | /games/{gameAddress} | Get a single game |
| GET | /games/{gameAddress}/stats | Game statistics |
| GET | /games/{gameAddress}/settings | Game settings |
| GET | /games/{gameAddress}/settings/{settingsId} | Single game setting |
| GET | /games/{gameAddress}/objectives | Game objectives |
| GET | /games/{gameAddress}/objectives/{objectiveId} | Single game objective |
| GET | /settings | Global settings |
| GET | /objectives | Global objectives |
| GET | /tokens | List tokens (filterable) |
| GET | /tokens/{tokenId} | Get a single token |
| GET | /tokens/{tokenId}/scores | Token score history |
| GET | /players/{address}/tokens | Player's tokens |
| GET | /players/{address}/stats | Player statistics |
| GET | /minters | List minters |
| GET | /minters/{minterId} | Get a single minter |
| GET | /activity | Activity events (filterable) |
| GET | /activity/stats | Activity statistics |
WebSocket Channels
Connect to the WebSocket URL and subscribe to real-time events:
| Channel | Description |
|---|---|
tokens | New token mints and updates |
scores | Score changes |
game_over | Game completion events |
mints | Mint 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: GameStatsTokens
// 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
| Parameter | Type | Description |
|---|---|---|
gameAddress | string | Filter by game contract address |
gameId | number | Filter by game ID |
owner | string | Filter by token owner |
settingsId | number | Filter by settings configuration |
objectiveId | number | Filter by objective |
minterAddress | string | Filter by who minted the token |
soulbound | boolean | Filter by soulbound status |
playable | boolean | Filter by playability |
gameOver | boolean | Filter by game completion |
mintedAfter | number | Filter by mint timestamp (after) |
mintedBefore | number | Filter by mint timestamp (before) |
includeUri | boolean | Fetch token URIs via batch RPC |
limit | number | Page size |
offset | number | Page 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: PlayerStatsMinters
// 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: MinterSee 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: ActivityStatsSee 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: GameObjectiveDetailsRPC 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.
| Method | Returns | Batch Variant |
|---|---|---|
tokenMetadata(tokenId) | TokenMetadata | tokenMetadataBatch(tokenIds) |
tokenMutableState(tokenId) | TokenMutableState | tokenMutableStateBatch(tokenIds) |
isPlayable(tokenId) | boolean | isPlayableBatch(tokenIds) |
settingsId(tokenId) | number | settingsIdBatch(tokenIds) |
playerName(tokenId) | string | playerNameBatch(tokenIds) |
objectiveId(tokenId) | number | objectiveIdBatch(tokenIds) |
mintedBy(tokenId) | string | mintedByBatch(tokenIds) |
isSoulbound(tokenId) | boolean | isSoulboundBatch(tokenIds) |
rendererAddress(tokenId) | string | rendererAddressBatch(tokenIds) |
tokenGameAddress(tokenId) | string | tokenGameAddressBatch(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...");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..");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..");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); // stringWrite 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 neededMintSaltCounter
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 0assignSalts
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 === 1Address 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";| Status | Meaning |
|---|---|
api | API is reachable, using primary data source |
rpc-fallback | API is unavailable, using RPC as fallback |
offline | Both 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
| Error | Description |
|---|---|
DenshokanError | Base error class |
ApiError | REST API returned an error (has statusCode) |
RpcError | Starknet RPC call failed |
RateLimitError | Rate limit exceeded (has retryAfter) |
TimeoutError | Request timed out |
AbortError | Request was aborted |
TokenNotFoundError | Token doesn't exist |
GameNotFoundError | Game doesn't exist |
InvalidChainError | Invalid chain specified |
DataSourceError | Both primary and fallback sources failed |
See Error Types for the full class definitions.
Cleanup
// Disconnect WebSocket and cleanup
client.disconnect();