| name | sentio-processor |
| description | 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. |
Managing Sentio Processors
Overview
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.
When to Use
- Creating/initializing a Sentio indexing project
- Adding contracts or ABIs to a project
- Writing processor code for any supported chain
- Testing processors with
TestProcessorServer
- Deploying processors with
sentio upload
- Configuring
sentio.yaml
- Multi-chain or multi-contract setups
- DeFi protocols (DEX, lending, staking, points systems)
Progressive Disclosure
This skill follows a layered approach:
- This file — CLI commands, core patterns, all handler types, metrics/events/store overview, best practices
- references/advanced-patterns.md — Multi-contract binding, GlobalProcessor, lazy caching, view calls, partition keys, baseLabels
- references/defi-patterns.md — Price feeds, DEX/AMM, lending protocols, TVL tracking, Aptos/Sui DEX helpers
- references/store-and-points.md — Store schema advanced features (relationships, timeseries, indexes, iterators, filter operators)
- references/position-tracking-templates.md — 10 protocol-specific points/position tracking templates (Simple Holding, Aave, Morpho, Vault/LP, Uni V3, Pendle, Compound, NFT, Uni V2, Uni V4)
- references/production-examples.md — 7 complete production processor examples (Uniswap, AAVE, Cetus, LiquidSwap, Lombard points, Scallop)
- references/sui-sdk4-migration.md — Upgrading to SDK 4: cross-chain breaking changes (dep bumps, Solana
@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 citations
Project Lifecycle
sentio create → sentio add → sentio gen → write processor → sentio test → sentio upload → sentio processor status
1. Initialize Project
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 |
2. Add Contracts & Generate Types
sentio add <address> --chain <chain> --name <Name>
sentio gen
sentio build
| 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) |
3. Login & Deploy
sentio login
sentio login --api-key <key>
sentio upload
sentio upload --skip-build
sentio upload --continue-from <ver>
sentio upload --checkpoint "1:18000000"
sentio upload --num-workers=4
4. Verify Processor is Running
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.
package.json
{
"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:
"overrides": {
"ethers": "npm:@sentio/ethers@6.13.1-patch.4"
}
sentio.yaml Quick Reference
project: owner/project-name
host: prod
contracts:
- chain: "1"
address: "0x..."
name: "USDC"
- chain: aptos_mainnet
address: "0x1"
- chain: sui_mainnet
address: "0xdee9..."
name: "deepbook"
networkOverrides:
- chain: '2390'
host: "https://rpc.tac.build"
variables:
- key: API_KEY
value: xxx
isSecret: true
debug: false
numWorkers: 1
Processor Patterns by Chain
Proxy contracts: Use the implementation contract's ABI while binding to the proxy address.
Ethereum / EVM
import { Counter, Gauge } from '@sentio/sdk'
import { EthChainId } from '@sentio/chain'
import { ERC20Processor } from '@sentio/sdk/eth/builtin'
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
Event Filtering
Filter events to only process matching ones (e.g., only mints):
const mintFilter = ERC20Processor.filters.Transfer(
"0x0000000000000000000000000000000000000000",
null
);
ERC20Processor.bind({ address: TOKEN, network: NETWORK })
.onEventTransfer(async (event, ctx) => {
}, mintFilter);
Generic Event Handler
Catches any event from the contract:
.onEvent(async (event, ctx) => {
});
Function Call Handler (onCallXxx)
Triggers on internal function calls (trace-based). Access args and return values:
MyContractProcessor.bind({ address: CONTRACT, network: NETWORK })
.onCallDeposit(async (call, ctx) => {
});
GlobalProcessor: GlobalProcessor.bind({ network }).onBlockInterval(handler)
Aptos
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)
Sui
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.
Solana
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)
})
Starknet
StarknetProcessor.bind({ address: '0x...', network: StarknetChainId.STARKNET_MAINNET })
.onEvent('Transfer', async (events, ctx) => {
ctx.meter.Counter('transfers').add(events.length)
})
Factory Pattern (ProcessorTemplate)
For dynamically-created contracts (e.g., DEX pair factory):
import { UniswapV2PairProcessorTemplate } from "./types/eth/uniswapv2pair.js";
import { UniswapV2FactoryProcessor } from "./types/eth/uniswapv2factory.js";
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);
});
UniswapV2FactoryProcessor.bind({
address: FACTORY_ADDRESS,
network: NETWORK,
})
.onEventPairCreated(async (event, ctx) => {
pairTemplate.bind(
{ address: event.args.pair, startBlock: ctx.blockNumber },
ctx
);
});
Metrics & Events
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')
const counter = Counter.register('name', { unit: 'token' })
counter.add(ctx, value, { label: 'value' })
ctx.meter.Counter('name').add(value, { label: 'value' })
ctx.meter.Gauge('name').record(value, { label: 'value' })
const vol = Gauge.register("vol", {
sparse: true,
aggregationConfig: {
intervalInMinutes: [60],
discardOrigin: true,
},
})
ctx.eventLogger.emit('EventName', {
distinctId: address,
message: 'description',
})
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.
Store (Database) API
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, [])
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.
Price Feeds
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.
Testing
import { TestProcessorServer, firstCounterValue } from '@sentio/sdk/testing'
const service = new TestProcessorServer(() => import('./processor.js'))
before(async () => { await service.start() })
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
sentio test --test-name-pattern="swap"
Project Structure
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)
Best Practices
- Use
sequential: true when handlers share state via entity store
- Do NOT use global variables for persistent state — use
ctx.store
- Use
startBlock to skip irrelevant history and reduce backfill cost
- Prefer Metrics over Logs/Entities for aggregated numerical data (lower cost)
- Never use addresses/hashes as metric labels — ~10k series limit per metric
- Use event filters (
Processor.filters) to reduce unnecessary handler executions
- Increase backfill intervals to the largest acceptable value for cost reduction
- Use
listIterator() instead of list() for large entity datasets
- Use
Promise.all for parallel contract calls and entity processing
- Use
BigDecimal and scaleDown() for precision — avoid floating point
- Filter null addresses with
isNullAddress() for mint/burn events
- Skip self-transfers —
if (from == to) return;
Common Mistakes
| 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 |