From sentio-ai-kit
Initializes, writes, tests, and deploys Sentio blockchain processors for multi-chain indexing and DeFi analytics.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sentio-ai-kit:sentio-processorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
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:
@anchor-lang/core, proto/RPC stack, dropped v3 back-compat) plus the Sui gRPC deep dive — raw data shapes (transaction/object/event, protobuf oneof, Move type-string comma spacing + address form), with Sui source citationssentio 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.
{
"name": "project-name",
"type": "module",
"scripts": {
"build": "sentio build",
"gen": "sentio gen",
"upload": "sentio upload",
"test": "sentio test"
},
"dependencies": {
"@sentio/sdk": "^4.0.0"
},
"devDependencies": {
"@sentio/cli": "^4.0.0",
"typescript": "^6.0.3"
}
}
ethers: SDK 4 uses upstream ethers 6.x — the old @sentio/ethers fork has been dropped. Do not pin or override ethers to @sentio/ethers. When migrating an older project, remove any leftover override such as:
// ❌ remove this from older projects — it reintroduces the fork and causes
// duplicate-ethers type conflicts (BaseContract / DeferredTopicFilter, TS2344)
"overrides": {
"ethers": "npm:@sentio/[email protected]"
}
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.
On SDK 4, Solana uses @anchor-lang/core + @solana/kit. Import BorshInstructionCoder (and Idl/Instruction) from @sentio/sdk/solana, not from @coral-xyz/anchor:
import { SolanaGlobalProcessor } from '@sentio/sdk/solana'
import { BorshInstructionCoder, type Idl } from '@sentio/sdk/solana'
SolanaGlobalProcessor.bind({
name: 'my-program',
instructionCoder: new BorshInstructionCoder(idl as 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 |
npx claudepluginhub sentioxyz/sentio-ai-kit --plugin sentio-ai-kitBuild, modify, or troubleshoot Sentio projects: write processor.ts handlers, run Sentio SQL queries, configure alerts and dashboards.
Builds web3 apps, scripts, CLIs, bots, and mobile/desktop clients that read blockchain data via the Blockscout PRO API — a single HTTP API spanning 100+ EVM chains.
Provides architectural rules, endpoint references, and output conventions for querying EVM blockchain data via the Blockscout MCP server. Use before making Blockscout API calls or writing on-chain data scripts.