From sentio-ai-kit
Use when initializing Sentio projects, writing blockchain processor code, adding contracts/ABIs, testing processors, or deploying to the Sentio platform. Triggers on sentio CLI usage, processor development, multi-chain indexing, blockchain data analytics with Sentio SDK, DeFi protocols, points systems, or store entities.
npx claudepluginhub sentioxyz/sentio-ai-kit --plugin sentio-ai-kitThis skill uses the workspace's default tool permissions.
Sentio SDK is a TypeScript blockchain data indexing platform. Processors handle on-chain events, transactions, and state changes across Ethereum, Aptos, Sui, Solana, Starknet, Bitcoin, Cosmos, Fuel, and IOTA.
Build, modify, or troubleshoot Sentio projects across processors, Sentio SQL in Data Studio, alerting, and dashboards. Use for tasks involving @sentio/sdk handlers in processor.ts, sentio.yaml config, metrics/event logs/entity store, running or sharing Sentio SQL queries, creating/updating alert rules and notification channels, or managing dashboards and panels.
Guides Somnia Reactivity pub/sub for reactive dApps: TypeScript WebSocket subscriptions, Solidity handlers, gas config, system events (BlockTick/Schedule), troubleshooting.
Configures Sentry for high-traffic apps handling 1M+ events/day with adaptive sampling, quota management, SDK benchmarking, batching, and k6 load testing.
Share bugs, ideas, or general feedback.
Sentio SDK is a TypeScript blockchain data indexing platform. Processors handle on-chain events, transactions, and state changes across Ethereum, Aptos, Sui, Solana, Starknet, Bitcoin, Cosmos, Fuel, and IOTA.
Reference examples: See sentioxyz/sentio-processors for 120+ production processors.
TestProcessorServersentio uploadsentio.yamlThis skill follows a layered approach:
sentio create → sentio add → sentio gen → write processor → sentio test → sentio upload → sentio processor status
npx @sentio/cli create <project-name> --chain-type <type> --chain-id <id>
| Flag | Description | Default |
|---|---|---|
--chain-type | eth, aptos, sui, solana, iota, fuel, starknet, raw | eth |
--chain-id | EVM chain ID (only for eth) | 1 |
--subproject | Monorepo mode (no root dependencies) | false |
sentio add <address> --chain <chain> --name <Name> # Downloads ABI, updates sentio.yaml
sentio gen # Generate TypeScript bindings
sentio build # Full: gen + typecheck + bundle
| Flag | Description |
|---|---|
-c, --chain | Chain ID (1, 56) or name (aptos_mainnet, sui_mainnet) |
-n, --name | Contract/module display name |
--api-key | Explorer API key (Etherscan) |
sentio login # OAuth browser flow (prod)
sentio login --api-key <key> # API key auth
sentio upload # Build + deploy
sentio upload --skip-build # Deploy only
sentio upload --continue-from <ver> # Hot-swap without re-indexing
sentio upload --checkpoint "1:18000000" # Rollback to specific block
sentio upload --num-workers=4 # Parallel workers for compute-heavy processors
After upload, use sentio processor status --project <owner>/<slug> to check the processor status. If the processor is in ERROR state, use sentio processor logs --project <owner>/<slug> --level ERROR to get error details, diagnose the issue, fix the code, and re-upload.
Recommend yarn over npm. With npm you may hit BaseContract / DeferredTopicFilter type conflicts (TS2344 due to duplicate ethers).
{
"name": "project-name",
"type": "module",
"scripts": {
"build": "sentio build",
"gen": "sentio gen",
"upload": "sentio upload",
"test": "sentio test"
},
"dependencies": {
"@sentio/sdk": "^3.4.0"
},
"devDependencies": {
"@sentio/cli": "^3.4.0",
"typescript": "^5.4.5"
}
}
If using npm and hitting ethers type conflicts, add:
"overrides": {
"ethers": "npm:@sentio/ethers@6.13.1-patch.4"
}
project: owner/project-name # Required
host: prod # prod | test | staging | local
contracts:
- chain: "1" # EVM: numeric IDs ("1", "56", "137", "42161")
address: "0x..."
name: "USDC"
- chain: aptos_mainnet # Move: aptos_mainnet, sui_mainnet, iota_mainnet
address: "0x1"
- chain: sui_mainnet
address: "0xdee9..."
name: "deepbook"
networkOverrides: # Redirect chain ID to custom RPC
- chain: '2390'
host: "https://rpc.tac.build"
variables: # Runtime env vars
- key: API_KEY
value: xxx
isSecret: true
debug: false
numWorkers: 1
Proxy contracts: Use the implementation contract's ABI while binding to the proxy address.
import { Counter, Gauge } from '@sentio/sdk'
import { EthChainId } from '@sentio/chain'
import { ERC20Processor } from '@sentio/sdk/eth/builtin'
// Or generated: import { MyContractProcessor } from './types/eth/mycontract.js'
const transfers = Counter.register('transfers')
ERC20Processor.bind({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
network: EthChainId.ETHEREUM,
startBlock: 6082465,
})
.onEventTransfer(async (event, ctx) => {
transfers.add(ctx, 1)
ctx.eventLogger.emit('Transfer', {
distinctId: event.args.from,
from: event.args.from,
to: event.args.to,
amount: event.args.value.scaleDown(6),
})
})
.onTimeInterval(async (block, ctx) => {}, 60, 240)
Handlers: onEvent*, onBlockInterval, onTimeInterval, onTransaction, onTrace, onCallXxx
Filter events to only process matching ones (e.g., only mints):
const mintFilter = ERC20Processor.filters.Transfer(
"0x0000000000000000000000000000000000000000", // from = null address (mint)
null // any recipient
);
ERC20Processor.bind({ address: TOKEN, network: NETWORK })
.onEventTransfer(async (event, ctx) => {
// only mint events reach here
}, mintFilter);
Catches any event from the contract:
.onEvent(async (event, ctx) => {
// event.eventName to determine which event
});
onCallXxx)Triggers on internal function calls (trace-based). Access args and return values:
MyContractProcessor.bind({ address: CONTRACT, network: NETWORK })
.onCallDeposit(async (call, ctx) => {
// call.args, call.returnValue, call.error
});
GlobalProcessor: GlobalProcessor.bind({ network }).onBlockInterval(handler)
import { AptosNetwork } from '@sentio/sdk/aptos'
import { coin } from '@sentio/sdk/aptos/builtin/0x1'
coin.bind({ network: AptosNetwork.MAIN_NET })
.onEventWithdrawEvent(async (evt, ctx) => {
ctx.meter.Counter('withdrawals').add(1)
ctx.eventLogger.emit('Withdraw', {
distinctId: evt.guid.account_address,
amount: evt.data_decoded.amount,
})
})
Handlers: onEvent*, onEntry*, onTransaction, onTimeInterval, onVersionInterval
Resources: AptosResourcesProcessor.bind({ address }).onResourceChange(handler, typeString)
import { SuiNetwork, SuiObjectTypeProcessor } from '@sentio/sdk/sui'
import { pool } from './types/sui/deepbook.js'
pool.bind({ network: SuiNetwork.MAIN_NET })
.onEventSwap(async (evt, ctx) => {
ctx.meter.Counter('swaps').add(1)
})
SuiObjectTypeProcessor.bind({ objectType: pool.Pool.type() })
.onObjectChange((changes, ctx) => {
ctx.meter.Counter('pool_updates').add(changes.length)
})
Handlers: onEvent*, onEntryFunctionCall, onObjectChange, onTimeInterval
Note: Use startCheckpoint: 8500000n (BigInt) instead of startBlock for Sui.
SolanaGlobalProcessor.bind({
name: 'my-program',
instructionCoder: new Anchor.BorshInstructionCoder(idl),
}).onInstruction('transfer', async (instruction, ctx, accounts) => {
ctx.meter.Counter('transfers').add(1)
})
StarknetProcessor.bind({ address: '0x...', network: StarknetChainId.STARKNET_MAINNET })
.onEvent('Transfer', async (events, ctx) => {
ctx.meter.Counter('transfers').add(events.length)
})
For dynamically-created contracts (e.g., DEX pair factory):
import { UniswapV2PairProcessorTemplate } from "./types/eth/uniswapv2pair.js";
import { UniswapV2FactoryProcessor } from "./types/eth/uniswapv2factory.js";
// 1. Define template with handlers
const pairTemplate = new UniswapV2PairProcessorTemplate()
.onEventSwap(async (event, ctx) => {
ctx.meter.Counter("swap_volume").add(event.args.amount0Out);
})
.onEventSync(async (event, ctx) => {
ctx.meter.Gauge("reserve0").record(event.args.reserve0);
});
// 2. Bind factory and dynamically bind new pairs
UniswapV2FactoryProcessor.bind({
address: FACTORY_ADDRESS,
network: NETWORK,
})
.onEventPairCreated(async (event, ctx) => {
pairTemplate.bind(
{ address: event.args.pair, startBlock: ctx.blockNumber },
ctx
);
});
CRITICAL — Metric Naming: Names MUST NOT end with reserved suffixes: _count, _sum, _avg, _min, _max, _last. These are appended automatically by Sentio. Error: COUNTER name "X" is invalid, cannot use [_count _sum _avg _min _max _last] as the suffix: invalid meta
❌ Counter.register('transfer_count') → invalid suffix _count
❌ ctx.meter.Counter('pool_sum') → invalid suffix _sum
✅ Counter.register('transfers')
✅ ctx.meter.Counter('pools_created')
// Global registration (recommended)
const counter = Counter.register('name', { unit: 'token' })
counter.add(ctx, value, { label: 'value' })
// Inline via context
ctx.meter.Counter('name').add(value, { label: 'value' })
ctx.meter.Gauge('name').record(value, { label: 'value' })
// Sparse gauge (high cardinality — many pools/tokens)
const vol = Gauge.register("vol", {
sparse: true,
aggregationConfig: {
intervalInMinutes: [60],
discardOrigin: true, // only keep aggregated vol_count, vol_sum
},
})
// Event logging
ctx.eventLogger.emit('EventName', {
distinctId: address, // Primary entity ID (used for user-level analytics)
message: 'description',
// ... arbitrary key-value attributes
// IMPORTANT: Numeric attributes used as time series metrics must be number or BigDecimal.
// Passing string/undefined/null for numeric fields causes "invalid time series data" errors.
})
// Exporter (export to external systems via webhook)
ctx.exporter.emit({ key: "value" })
Label cardinality warning: ~10,000 unique series limit per metric. NEVER use wallet addresses, tx hashes, or token IDs as labels. Use event logs or entities for high-cardinality data instead.
Define entities in schema.graphql, generated by sentio gen:
type AccountSnapshot @entity {
id: ID!
timestampMilli: BigInt!
balance: BigInt!
}
import { AccountSnapshot } from "./schema/store.js"
await ctx.store.get(AccountSnapshot, id)
await ctx.store.upsert(new AccountSnapshot({ id, timestampMilli, balance }))
await ctx.store.list(AccountSnapshot, []) // List all
await ctx.store.delete(AccountSnapshot, id)
Critical: Enable sequential execution when using store to prevent race conditions:
import { GLOBAL_CONFIG } from "@sentio/runtime"
GLOBAL_CONFIG.execution = { sequential: true }
See references/store-and-points.md for points system patterns.
import { getPriceByType, token } from "@sentio/sdk/utils"
const info = await token.getERC20TokenInfo(EthChainId.ETHEREUM, tokenAddr)
const price = await getPriceByType(EthChainId.ETHEREUM, tokenAddr, ctx.timestamp) || 0
const usdValue = amount.scaleDown(info.decimal).multipliedBy(price)
See references/defi-patterns.md for caching patterns and DeFi examples.
import { TestProcessorServer, firstCounterValue } from '@sentio/sdk/testing'
const service = new TestProcessorServer(() => import('./processor.js'))
before(async () => { await service.start() })
// Ethereum
const resp = await service.eth.testLog(mockTransferLog('0x...', { from, to, value }))
assert.equal(firstCounterValue(resp.result, 'transfers'), 1n)
| Chain | Facet | Key Methods |
|---|---|---|
| Ethereum | service.eth | testLog(), testBlock(), testTransaction(), testTrace() |
| Aptos | service.aptos | testEvent(), testCall(), testResourceChange() |
| Sui | service.sui | testEvent(), testObjectChange() |
| Solana | service.solana | testInstruction() |
sentio test # All tests
sentio test --test-name-pattern="swap" # Filter by name
my-project/
sentio.yaml # Project config
schema.graphql # Store entity definitions (optional)
package.json # @sentio/sdk + @sentio/cli deps
tsconfig.json
abis/{chain}/ # Contract ABIs
src/
processor.ts # Main processor code
processor.test.ts # Tests
types/ # Auto-generated (sentio gen)
schema/ # Auto-generated store entities
dist/lib.js # Bundled output (sentio build)
sequential: true when handlers share state via entity storectx.storestartBlock to skip irrelevant history and reduce backfill costProcessor.filters) to reduce unnecessary handler executionslistIterator() instead of list() for large entity datasetsPromise.all for parallel contract calls and entity processingBigDecimal and scaleDown() for precision — avoid floating pointisNullAddress() for mint/burn eventsif (from == to) return;| Mistake | Fix |
|---|---|
Metric name ends with _count, _sum, _avg, _min, _max, _last | Use a different name: transfers not transfer_count, pools_created not pool_count |
| "invalid time series data" in eventLogger | Ensure numeric fields are number or BigDecimal, not string/undefined/null; use || 0 for fallible prices |
Writing processor before sentio gen | Always sentio add then sentio gen first |
| Wrong import path for generated types | Use ./types/{chain}/{name}.js (.js extension for ESM) |
Forgetting .scaleDown(decimals) | .scaleDown(18) for ETH, .scaleDown(6) for USDC |
Not setting startBlock / startCheckpoint | Processor starts from genesis without it |
| Store entities without sequential execution | Add GLOBAL_CONFIG.execution = { sequential: true } |
| Deploying without login | Run sentio login first |
| Caching resolved values instead of Promises | Cache the Promise to prevent duplicate concurrent RPC calls |