An event-driven arbitrage bot, written in TypeScript and run with Bun current set on Sei EVM chain.
It discovers and tracks V2 pools, configured V3 pools, and Carbon strategies, keeps their live state in an in-memory graph, searches mixed-protocol routes, accounts for swap and flash-loan fees, and submits profitable routes through the arbitrage contract.
This is trading code, not a toaster. It can sign real transactions and lose real money. Read the config before giving it a funded key.
protocol config -> market sync -> SQLite catalog
|
v
events -> startup buffer -> live-state hydration -> market graph
|
v
search -> size -> execute
Startup subscribes to market events before fetching live state. Events received during hydration are buffered and reconciled before the first search, so the graph does not begin life one block behind.
Each protocol is a plugin under src/protocols/<protocol>/ with the same public layout:
config events execution metadata quote runtime types index
Shared code owns graph search, scheduling, persistence, and transaction submission. Protocol plugins own their discovery, state, event decoding, quotes, fees, and calldata details.
Start by copying the public shape, not another protocol's internals:
src/protocols/my-protocol/
config.ts addresses and tuning knobs
events.ts ABIs and event decoding
execution.ts fees and execution encoding
metadata.ts market discovery/catalog data
quote.ts protocol-specific swap math
runtime.ts hydration and live event adapter
types.ts protocol-owned data types
index.ts the ProtocolPlugin implementation
Then wire the boring bits:
- Implement
ProtocolPluginfromsrc/protocols/protocol-plugin.ts. Give it anid, uniquecontractId,discover,hydrate,events, andcount. - Add any new catalog/graph types to the shared market model. Reuse existing shapes when they are genuinely compatible; fake compatibility gets expensive fast.
- Register the plugin in
src/protocols/registry.ts. Order matters when one plugin's discovery depends on another, like Carbon using the V2/V3 token universe. - Add matching route execution support in
Contract/NArb.sol. The TypeScriptcontractIdand Solidity protocol ID must agree or the bot will confidently encode nonsense. - Run
bun run sync:markets, then typecheck and test. A plugin is done when discovery, startup hydration, event updates, quotes, and execution all describe the same market.
The shared runtime already handles startup buffering, scheduling, and reconnects. A plugin should provide protocol facts, not build its own mini bot.
Requires Bun and a RPC endpoint.
bun install
cp .env.example .envFill in .env, then review:
src/constants.tsfor tokens, search limits, profit thresholds, gas, and execution policy.src/protocols/*/config.tsfor factories, pools, controllers, fees, and batching.src/networkto change network
EXECUTION_POLICY.executeTrades is currently true. That is intentionally loud here.
Build the local market catalog first:
bun run sync:marketsThis writes V2 pool metadata, V3 pool metadata, and Carbon pair metadata to data/markets.sqlite. Re-run it after changing protocol or token configuration.
Start the bot:
bun startThe bot loads SQLite, starts the event feed in buffering mode, hydrates current protocol state, reconciles startup events, runs the initial search, and then scans again whenever tracked state changes.
bunx tsc --noEmit
bun test
bun run test:stress
bun run bench:stressThe stress test accepts V2_STRESS_PAIRS, V2_STRESS_SEARCH_LIMIT_MS, and V2_STRESS_UPDATES overrides.
src/protocols/ protocol plugins: V2, V3, Carbon
src/market-graph/ graph topology and quote traversal
src/opportunities/ search, sizing, ranking, and workflow
src/runtime/ bot lifecycle, event transport, scheduling
src/execution/ route planning and contract call construction
src/market-db.ts SQLite market catalog
Contract/ on-chain arbitrage contract
test/ unit and stress tests
Private keys belong in .env, which is ignored by Git. .env.example contains placeholders only. Do not get creative with that rule.