Build a Polymarket On-Chain Bot
In this guide, you’ll learn how to create a Polymarket bot that monitors Ethereum addresses directly on-chain and sends real-time human-readable alerts about Polymarket trades.
What makes this approach powerful:
- Monitor transactions directly via RPC WebSocket
- Decode and interpret transactions using only the transaction hash - no external APIs needed
- Get human-readable descriptions like “Bought 100 YES shares” automatically

Guide
Step 0: Prerequisites
- Bun installed (see installation guide here)
- Etherescan API Key (sign up here)
- Polygon WebSocket RPC URL (you can get one from Alchemy)
- (Optional) Neynar API Key to publish messages on Farcaster
Step 1: Clone the Repository
Clone the bot repository and install dependencies:
git clone https://github.com/3loop/polymarket-botcd polymarket-botbun iStep 2: Configure Environment Variables
Copy the .env.example file to .env and add your API keys:
cp .env.example .envvim .envFor the Polymarket bot you need to specify the following keys:
RPC_URL- Polygon mainnet RPC URL for transaction decoding (non-archive node is sufficient)WS_RPC_URL- WebSocket URL for real-time subscription (can be the same asRPC_URLwithwssprotocol if supported)ETHERSCAN_API_KEY- Etherescan V2 API key, used in transaction decoding to fetch and cache ABIs- (optional)
NEYNAR_API_KEYandNEYNAR_SIGNER_UUID- to resolve Polymarket wallet addresses and publish messages to Farcaster
Step 3: Setup the Transaciton Decoder
Loop Decoder requires three components: an RPC provider, ABI store, and contract metadata store. Let’s set up each one:
RPC Provider
Configure your RPC provider in constants.ts for Polygon Mainnet (chain ID 137). We set traceAPI to 'none' since we do not use traces to understand Polymarket transactions in the interpretation:
export const RPC = { 137: { url: process.env.RPC_URL, traceAPI: 'none', },}export const getPublicClient = (chainId: number) => { const rpc = RPC[chainId as keyof typeof RPC] if (!rpc) throw new Error(`Missing RPC provider for chain ID ${chainId}`)
return { client: createPublicClient({ transport: http(rpc.url) }), config: { traceAPI: rpc.traceAPI }, }}ABI Store
Set up an in-memory ABI cache with Etherscan and 4byte.directory strategies:
import { EtherscanStrategyResolver, FourByteStrategyResolver, VanillaAbiStore, ContractABI,} from '@3loop/transaction-decoder'
// Create an in-memory cache for the ABIsconst abiCache = new Map<string, ContractABI>()
const abiStore: VanillaAbiStore = { strategies: [ // List of stratagies to resolve new ABIs EtherscanV2StrategyResolver({ apikey: process.env.ETHERSCAN_API_KEY || '', }), FourByteStrategyResolver(), ],
// Get ABI from memory by address, event or signature // Can be returned the list of all possible ABIs get: async ({ address, event, signature }) => { const key = address?.toLowerCase() || event || signature if (!key) return []
const cached = abiCache.get(key) return cached ? [ { ...cached, id: key, source: 'etherscan', status: 'success', }, ] : [] },
set: async (_key, abi) => { const key = abi.type === 'address' ? abi.address.toLowerCase() : abi.type === 'event' ? abi.event : abi.type === 'func' ? abi.signature : null
if (key) abiCache.set(key, abi) },}Contract Metadata Store
Set up contract metadata resolution for token or NFT information (name, decimals, symbol):
import type { ContractData, VanillaContractMetaStore } from '@3loop/transaction-decoder'import { ERC20RPCStrategyResolver, NFTRPCStrategyResolver } from '@3loop/transaction-decoder'
// Create an in-memory cache for the contract meta-informationconst contractMetaCache = new Map<string, ContractData>()
const contractMetaStore: VanillaContractMetaStore = { strategies: [ERC20RPCStrategyResolver, NFTRPCStrategyResolver],
get: async ({ address, chainID }) => { const key = `${address}-${chainID}`.toLowerCase() const cached = contractMetaCache.get(key) return cached ? { status: 'success', result: cached } : { status: 'empty', result: null } },
set: async ({ address, chainID }, result) => { if (result.status === 'success') { contractMetaCache.set(`${address}-${chainID}`.toLowerCase(), result.result) } },}Create Decoder Instance
Combine all components into a TransactionDecoder instance:
import { TransactionDecoder } from '@3loop/transaction-decoder'
export const decoder = new TransactionDecoder({ getPublicClient, abiStore, contractMetaStore,})Step 4: Setup the Transaciton Interpreter
The interpreter converts DecodedTransaction into human-readable descriptions. Here’s the implementation in src/decoder/interpreter.ts:
import { getInterpreter, fallbackInterpreter } from '@3loop/transaction-interpreter'
export async function interpretTransaction(decodedTx: DecodedTransaction, userAddress?: string) { // Find the right interpreter for this transaction const interpreter = getInterpreter(decodedTx) ?? fallbackInterpreter
// Configure for Polymarket (needs off-chain data fetching) const config = Layer.succeed(QuickjsConfig, { variant: variant, runtimeConfig: { timeout: 5000, useFetch: true, // Required for Polymarket market data }, })
// Apply the interpreter const layer = Layer.provide(QuickjsInterpreterLive, config) const runnable = Effect.gen(function* () { const service = yield* TransactionInterpreter return yield* service.interpretTransaction(decodedTx, interpreter, { interpretAsUserAddress: userAddress, // Get user's perspective (bought vs sold) }) }).pipe(Effect.provide(layer))
return Effect.runPromise(runnable)}The userAddress parameter is important - it determines the transaction direction from the user’s perspective (e.g., “bought” vs “sold” shares).
Step 5: Decode and Interpret Transactions
With the decoder set up, you can now decode transactions and make them human-readable. Here’s how it works:
// 1. Decode the transactionconst decoded = await decoder.decodeTransaction({ chainID: CHAIN_ID, hash: txHash,})
// 2. Interpret it (make it human-readable)const interpreted = await interpretTransaction(decoded, userAddress)
// 3. Use the resultconsole.log(interpreted.action) // e.g., "Bought 100 YES shares in market 'Will Trump win?'"View a decoded transaction example in our playground.
Step 6: Monitor Polymarket Transactions
Now let’s set up real-time monitoring for Polymarket OrderFilled events:
export const POLYMARKET_EXCHANGE_ADDRESS = '0x4bfb41d5b3570defd03c39a9a4d8de6bd8b8982e'export const CHAIN_ID = 137// List of Polymarket user addresses to trackconst ADDRESSES_TO_TRACK = ['0xca85f4b9e472b542e1df039594eeaebb6d466bf2']
// Subscribe to OrderFilled eventswsClient.watchEvent({ address: POLYMARKET_EXCHANGE_ADDRESS, event: ORDER_FILLED_EVENT_ABI, args: { maker: ADDRESSES_TO_TRACK, }, onLogs: (logs) => { logs.forEach((log) => { handleTransaction(log.transactionHash, log.args.maker) }) },})
// Process each transactionasync function handleTransaction(txHash: string, userAddress: string) { // 1. Decode const decoded = await decoder.decodeTransaction({ chainID: CHAIN_ID, hash: txHash, })
// 2. Interpret from user's perspective const interpreted = await interpretTransaction(decoded, userAddress)
// 3. Create message const message = { action: interpreted.action, txHash, userAddress, }}Step 7: (Optional) Publish to Farcaster
If you configured Neynar API keys, you can resolve Farcaster usernames and publish alerts:
async function publishToFarcaster(message: { action: string; txHash: string; userAddress: string }) { const farcasterUser = await resolveFarcasterUser(message.userAddress)
const castText = farcasterUser ? `@${farcasterUser.username} ${message.action}` : `${message.action}`
await neynarClient.publishCast({ text: castText, embeds: [{ url: `https://polygonscan.com/tx/${message.txHash}` }], })}Step 8: Run the Bot
Start the bot locally:
bun run src/index.tsThe bot will now monitor Polymarket transactions for your tracked addresses and publish human-readable alerts to Farcaster.
Next Steps
You’ve built a Polymarket bot that:
- Monitors specific addresses in real-time from the blockchain directly
- Decodes Polymarket transactions
- Generates human-readable descriptions
- Posts alerts to Farcaster
Customize it further:
- Track different Polymarket addresses by updating
ADDRESSES_TO_TRACK - Modify the message format in
publishToFarcaster - Add filters for specific market types or trade sizes
- Deploy to a server for 24/7 monitoring
Need help? Reach out on X/Twitter @3loop_io or check the full code example.