
Opportunity
- 12 installs
- 610 repo stars
- Updated June 26, 2026
- alsk1992/cloddsbot
Opportunity (in cloddsbot) is a skill that finds and executes cross-platform arbitrage across prediction markets like Polymarket, Kalshi, and Betfair.
About
This skill scans multiple prediction-market platforms to find and execute cross-platform arbitrage opportunities. A developer uses it to detect price discrepancies, link equivalent markets across platforms, model execution risk, and place trades. It supports internal, cross-platform, combinatorial, and edge opportunity types and can run continuous real-time scans.
- Finds cross-platform arbitrage across Polymarket, Kalshi, Betfair, Manifold and more
- Detects internal, cross-platform, combinatorial, and edge opportunities
- Real-time scanning, market linking, execution, and Kelly/risk modeling
Opportunity by the numbers
- 12 all-time installs (skills.sh)
- Ranked #781 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
opportunity capabilities & compatibility
Gated on POLY_API_KEY or KALSHI_API_KEY; platform trading fees apply.
- Use cases
- trading
- Pricing
- Bring your own API key
What opportunity says it does
Find and execute cross-platform arbitrage opportunities across prediction markets
which found **$40M+ in realized arbitrage** on Polymarket
npx skills add https://github.com/alsk1992/cloddsbot --skill opportunityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 610 |
| Last updated | June 26, 2026 |
| Repository | alsk1992/cloddsbot ↗ |
What it does
Detect and execute arbitrage across prediction markets like Polymarket and Kalshi.
When should I use this skill?
You want to scan prediction markets for arbitrage edges and execute them.
What you get
Ranked arbitrage opportunities with edge, liquidity, and Kelly sizing, ready to execute.
By the numbers
- 4 opportunity types
- 8 supported platforms
- $40M+ realized arbitrage cited from arXiv:2508.03474
Files
Opportunity Finder - Complete API Reference
Discover and execute cross-platform arbitrage opportunities across Polymarket, Kalshi, Betfair, Smarkets, Manifold, Metaculus, PredictIt, and Drift.
Based on arXiv:2508.03474 which found $40M+ in realized arbitrage on Polymarket.
Opportunity Types
| Type | Description | Example |
|---|---|---|
| Internal | YES + NO < $1 on same platform | Buy both for guaranteed profit |
| Cross-Platform | Same market priced differently | Buy low on A, sell high on B |
| Combinatorial | Logical violations (P(A) > P(B) when A implies B) | Trump > Republican |
| Edge | Market vs external model (538, polls) | Market 45%, model 52% |
---
Chat Commands
Scanning
/opportunities scan # Scan all platforms for opportunities
/opportunities scan "trump" # Scan with keyword filter
/opportunities scan --min-edge 2 # Min 2% edge
/opportunities scan --min-liquidity 1000 # Min $1000 liquidity
/opportunities active # View active opportunities
/opportunities active --sort edge # Sort by edge size
/opportunities active --sort liquidity # Sort by liquidityReal-Time Monitoring
/opportunities realtime start # Start continuous scanning
/opportunities realtime stop # Stop scanning
/opportunities realtime status # Check monitoring status
/opportunities realtime config --interval 30 # Set scan interval (seconds)Market Linking
/opportunities link <market-a> <market-b> # Manually link equivalent markets
/opportunities unlink <market-a> <market-b> # Remove link
/opportunities links # View all linked markets
/opportunities auto-match # Run auto-matching algorithmExecution
/opportunities execute <id> # Execute an opportunity
/opportunities execute <id> --size 100 # Execute with $100 size
/opportunities mark-taken <id> # Mark as taken (manual)
/opportunities record-outcome <id> <pnl> # Record P&L outcomeAnalytics
/opportunities stats # Performance statistics
/opportunities stats --period 7d # Last 7 days
/opportunities history # Past opportunities
/opportunities by-platform # Stats by platform pair
/opportunities by-type # Stats by opportunity typeRisk Modeling
/opportunities risk <id> # Model execution risk
/opportunities estimate <id> # Estimate execution costs
/opportunities kelly <id> # Calculate Kelly fraction---
TypeScript API Reference
Create Opportunity Finder
import { createOpportunityFinder } from 'clodds/opportunity';
const finder = createOpportunityFinder({
platforms: ['polymarket', 'kalshi', 'betfair', 'manifold'],
// Filtering
minEdge: 0.5, // 0.5% minimum edge
minLiquidity: 500, // $500 minimum liquidity
minConfidence: 0.7, // 70% match confidence
// Real-time
enableRealtime: true,
scanIntervalMs: 30000, // 30 second intervals
// Credentials
polymarket: { apiKey, apiSecret, passphrase, privateKey },
kalshi: { apiKey, privateKey },
});Scan for Opportunities
// One-time scan
const opportunities = await finder.scan({
query: 'election', // Optional keyword
minEdge: 1, // 1% minimum
minLiquidity: 1000, // $1000 minimum
platforms: ['polymarket', 'kalshi'],
});
for (const opp of opportunities) {
console.log(`${opp.type}: ${opp.description}`);
console.log(` Edge: ${opp.edge.toFixed(2)}%`);
console.log(` Liquidity: $${opp.liquidity.toLocaleString()}`);
console.log(` Confidence: ${(opp.confidence * 100).toFixed(0)}%`);
console.log(` Score: ${opp.score}/100`);
console.log(` Platforms: ${opp.platforms.join(' ↔ ')}`);
}Real-Time Monitoring
// Start real-time scanning
await finder.startRealtime();
// Event handlers
finder.on('opportunity', (opp) => {
console.log(`🎯 New opportunity: ${opp.description}`);
console.log(` Edge: ${opp.edge.toFixed(2)}%`);
});
finder.on('opportunityExpired', (opp) => {
console.log(`❌ Opportunity expired: ${opp.id}`);
});
finder.on('opportunityUpdated', (opp) => {
console.log(`📊 Updated: ${opp.id} - Edge now ${opp.edge.toFixed(2)}%`);
});
// Get active opportunities
const active = await finder.getActive();
// Stop monitoring
await finder.stopRealtime();Market Linking
// Manually link equivalent markets
await finder.linkMarkets(
{ platform: 'polymarket', id: 'market-123' },
{ platform: 'kalshi', id: 'TRUMP-WIN' }
);
// Auto-match using semantic similarity
const matches = await finder.autoMatchMarkets({
minSimilarity: 0.85,
platforms: ['polymarket', 'kalshi'],
});
console.log(`Found ${matches.length} potential matches`);
for (const match of matches) {
console.log(`${match.marketA.question}`);
console.log(` ↔ ${match.marketB.question}`);
console.log(` Similarity: ${(match.similarity * 100).toFixed(0)}%`);
}
// Get all links
const links = await finder.getLinks();Execute Opportunity
// Execute an opportunity
const result = await finder.execute(opportunityId, {
size: 100, // $100 position
maxSlippage: 0.5, // 0.5% max slippage
useProtectedOrders: true,
});
console.log(`Executed: ${result.status}`);
console.log(` Filled: $${result.filledSize}`);
console.log(` Avg price: ${result.avgPrice}`);
console.log(` Fees: $${result.fees}`);
// Mark as taken manually
await finder.markTaken(opportunityId);
// Record outcome
await finder.recordOutcome(opportunityId, {
pnl: 25.50,
exitPrice: 0.55,
exitTimestamp: Date.now(),
});Analytics
// Get statistics
const stats = await finder.getAnalytics({
period: '30d',
});
console.log(`Total opportunities: ${stats.total}`);
console.log(`Taken: ${stats.taken}`);
console.log(`Win rate: ${(stats.winRate * 100).toFixed(1)}%`);
console.log(`Total P&L: $${stats.totalPnl.toLocaleString()}`);
console.log(`Avg edge: ${stats.avgEdge.toFixed(2)}%`);
console.log(`By platform pair:`);
for (const [pair, data] of Object.entries(stats.byPlatformPair)) {
console.log(` ${pair}: ${data.count} opps, $${data.pnl} P&L`);
}Risk Modeling
// Model execution risk
const risk = await finder.modelRisk(opportunityId);
console.log(`Execution risk:`);
console.log(` Fill probability: ${(risk.fillProbability * 100).toFixed(0)}%`);
console.log(` Expected slippage: ${risk.expectedSlippage.toFixed(2)}%`);
console.log(` Time to fill: ${risk.estimatedTimeToFill}s`);
console.log(` Counterparty risk: ${risk.counterpartyRisk}`);
// Estimate execution
const estimate = await finder.estimateExecution(opportunityId, {
size: 500,
});
console.log(`Execution estimate for $500:`);
console.log(` Expected fill: $${estimate.expectedFill}`);
console.log(` Expected cost: $${estimate.expectedCost}`);
console.log(` Net edge after costs: ${estimate.netEdge.toFixed(2)}%`);---
Opportunity Scoring
Opportunities are scored 0-100 based on:
| Factor | Weight | Description |
|---|---|---|
| Edge % | 35% | Raw arbitrage spread |
| Liquidity | 25% | Available volume |
| Confidence | 25% | Match quality |
| Execution | 15% | Platform reliability |
Penalties
- Low liquidity (<$1000): -5 points
- Cross-platform complexity: -3 per platform
- High slippage (>2%): -5 points
- Low confidence (<70%): -5 points
- Near expiry (<24h): -3 points
---
Semantic Matching
Markets are matched using:
1. Exact slug match - Platform-specific IDs 2. Text similarity - Jaccard coefficient 3. Vector embeddings - Semantic similarity 4. Manual links - User-defined
// Configure matching
finder.setMatchingConfig({
minTextSimilarity: 0.8,
minEmbeddingSimilarity: 0.85,
useManualLinksFirst: true,
});---
Best Practices
1. Start with high-confidence matches - 85%+ similarity 2. Check liquidity - Ensure enough volume to execute 3. Account for fees - Factor in platform fees 4. Use protected orders - Avoid slippage 5. Monitor in real-time - Opportunities disappear fast 6. Track outcomes - Build performance history
/**
* Opportunity Finder CLI Skill
*
* Commands:
* /opportunities scan [query] - Scan for cross-platform arbitrage
* /opportunities active - View active opportunities
* /opportunities realtime start|stop|status - Real-time monitoring
* /opportunities link <a> <b> - Link equivalent markets
* /opportunities unlink <a> <b> - Remove link
* /opportunities links - View all linked markets
* /opportunities execute <id> [--size N] - Execute an opportunity
* /opportunities mark-taken <id> - Mark as taken
* /opportunities stats [--period Nd] - Performance statistics
* /opportunities history - Past opportunities
* /opportunities risk <id> - Model execution risk
* /opportunities kelly <id> - Calculate Kelly fraction
*/
import {
createOpportunityFinder,
OpportunityFinder,
Opportunity,
} from '../../../opportunity/index';
import { logger } from '../../../utils/logger';
let finder: OpportunityFinder | null = null;
function formatOpportunity(opp: Opportunity): string {
let output = `**${opp.type.replace('_', ' ').toUpperCase()}** (Score: ${opp.score}/100)\n`;
output += ` ID: \`${opp.id.slice(0, 40)}\`\n`;
output += ` Edge: ${opp.edgePct.toFixed(2)}%\n`;
output += ` Profit per $100: $${opp.profitPer100.toFixed(2)}\n`;
output += ` Liquidity: $${opp.totalLiquidity.toLocaleString()}\n`;
output += ` Confidence: ${(opp.confidence * 100).toFixed(0)}%\n`;
if (opp.kellyFraction > 0) {
output += ` Kelly: ${(opp.kellyFraction * 100).toFixed(1)}%\n`;
}
for (const m of opp.markets) {
output += ` ${m.platform}: ${m.action.toUpperCase()} ${m.outcome} @ ${(m.price * 100).toFixed(1)}c\n`;
output += ` "${m.question.slice(0, 60)}${m.question.length > 60 ? '...' : ''}"\n`;
}
if (opp.matchVerification) {
output += ` Match: ${opp.matchVerification.method} (${(opp.matchVerification.similarity * 100).toFixed(0)}%)\n`;
if (opp.matchVerification.warnings?.length) {
output += ` Warnings: ${opp.matchVerification.warnings.join('; ')}\n`;
}
}
output += ` Status: ${opp.status}\n`;
return output;
}
async function handleScan(query?: string, flags?: Record<string, string>): Promise<string> {
if (!finder) return 'Opportunity finder not initialized. Platform API keys required (POLY_API_KEY or KALSHI_API_KEY).';
try {
const minEdge = flags?.['min-edge'] ? parseFloat(flags['min-edge']) : undefined;
const minLiquidity = flags?.['min-liquidity'] ? parseFloat(flags['min-liquidity']) : undefined;
const opps = await finder.scan({
query: query || undefined,
minEdge,
minLiquidity,
limit: 20,
});
if (opps.length === 0) {
return 'No opportunities found matching criteria.';
}
let output = `**Opportunities Found** (${opps.length})\n\n`;
for (const opp of opps) {
output += formatOpportunity(opp) + '\n';
}
return output;
} catch (error) {
return `Error scanning: ${error instanceof Error ? error.message : String(error)}`;
}
}
async function handleActive(sortBy?: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
const active = finder.getActive();
if (active.length === 0) {
return 'No active opportunities. Run `/opportunities scan` to find some.';
}
const sorted = [...active];
if (sortBy === 'edge') sorted.sort((a, b) => b.edgePct - a.edgePct);
else if (sortBy === 'liquidity') sorted.sort((a, b) => b.totalLiquidity - a.totalLiquidity);
else sorted.sort((a, b) => b.score - a.score);
let output = `**Active Opportunities** (${sorted.length})\n\n`;
for (const opp of sorted) {
output += formatOpportunity(opp) + '\n';
}
return output;
}
async function handleRealtime(action: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
switch (action) {
case 'start':
await finder.startRealtime();
return 'Real-time opportunity scanning started.';
case 'stop':
finder.stopRealtime();
return 'Real-time opportunity scanning stopped.';
case 'status': {
const active = finder.getActive();
return `**Real-time Status**\n\nActive opportunities: ${active.length}`;
}
default:
return 'Usage: /opportunities realtime start|stop|status';
}
}
async function handleLink(marketA: string, marketB: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
if (!marketA || !marketB) return 'Usage: /opportunities link <market-a> <market-b>';
finder.linkMarkets(marketA, marketB);
return `Markets linked: ${marketA} <-> ${marketB}`;
}
async function handleUnlink(marketA: string, marketB: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
if (!marketA || !marketB) return 'Usage: /opportunities unlink <market-a> <market-b>';
finder.unlinkMarkets(marketA, marketB);
return `Markets unlinked: ${marketA} <-> ${marketB}`;
}
async function handleStats(period?: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
const days = period ? parseInt(period.replace('d', '')) : 30;
const stats = finder.getAnalytics({ days: isNaN(days) ? 30 : days });
let output = `**Opportunity Statistics** (${days}d)\n\n`;
output += `Total found: ${stats.totalFound}\n`;
output += `Taken: ${stats.taken}\n`;
output += `Win rate: ${stats.winRate.toFixed(1)}%\n`;
output += `Total profit: $${stats.totalProfit.toLocaleString()}\n`;
output += `Avg edge: ${stats.avgEdge.toFixed(2)}%\n`;
if (stats.bestPlatformPair) {
output += `\nBest pair: ${stats.bestPlatformPair.platforms.join(' <-> ')}\n`;
output += ` Win rate: ${stats.bestPlatformPair.winRate.toFixed(1)}%\n`;
output += ` Profit: $${stats.bestPlatformPair.profit.toLocaleString()}\n`;
}
return output;
}
async function handleMarkTaken(id: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
if (!id) return 'Usage: /opportunities mark-taken <id>';
finder.markTaken(id);
return `Opportunity ${id.slice(0, 30)}... marked as taken.`;
}
async function handleRisk(id: string): Promise<string> {
if (!finder) return 'Opportunity finder not initialized.';
if (!id) return 'Usage: /opportunities risk <id>';
const opp = finder.get(id);
if (!opp) return `Opportunity ${id} not found.`;
const risk = finder.modelRisk(opp, 100) as unknown as Record<string, number>;
let output = `**Risk Model: ${id.slice(0, 30)}...**\n\n`;
output += `Fill probability: ${((risk.fillProbability ?? 0) * 100).toFixed(0)}%\n`;
output += `Expected slippage: ${(risk.expectedSlippage ?? 0).toFixed(2)}%\n`;
output += `Net expected edge: ${(risk.netExpectedEdge ?? 0).toFixed(2)}%\n`;
output += `Recommended size: $${(risk.recommendedSize ?? 0).toFixed(2)}\n`;
output += `Risk level: ${risk.riskLevel}\n`;
return output;
}
function parseFlags(parts: string[]): { args: string[]; flags: Record<string, string> } {
const args: string[] = [];
const flags: Record<string, string> = {};
for (let i = 0; i < parts.length; i++) {
if (parts[i].startsWith('--')) {
const key = parts[i].slice(2);
flags[key] = parts[i + 1] || 'true';
i++;
} else {
args.push(parts[i]);
}
}
return { args, flags };
}
export async function execute(args: string): Promise<string> {
const parts = args.trim().split(/\s+/);
const command = parts[0]?.toLowerCase() || 'help';
const rest = parts.slice(1);
const { args: restArgs, flags } = parseFlags(rest);
if (!finder) {
try {
const { createDatabase } = await import('../../../db/index');
const { createFeedManager } = await import('../../../feeds/index');
const db = createDatabase();
const feeds = await createFeedManager({} as any);
finder = createOpportunityFinder(db, feeds);
} catch { /* leave null if dependencies missing */ }
}
switch (command) {
case 'scan':
case 'search':
return handleScan(restArgs.join(' ') || undefined, flags);
case 'active':
return handleActive(flags.sort);
case 'realtime':
return handleRealtime(restArgs[0] || '');
case 'link':
return handleLink(restArgs[0], restArgs[1]);
case 'unlink':
return handleUnlink(restArgs[0], restArgs[1]);
case 'links': {
if (!finder) return 'Opportunity finder not initialized.';
const pairs = finder.getPlatformPairs();
if (pairs.length === 0) return 'No linked platform pairs.';
let output = '**Platform Pairs**\n\n';
for (const pair of pairs) {
output += `${pair.platforms.join(' <-> ')}: ${pair.count} opportunities, avg edge ${pair.avgEdge.toFixed(2)}%\n`;
}
return output;
}
case 'execute':
if (!restArgs[0]) return 'Usage: /opportunities execute <id> [--size N]';
return `Execution not available in CLI mode. Use the TypeScript API to execute opportunity ${restArgs[0]}.`;
case 'mark-taken':
return handleMarkTaken(restArgs[0]);
case 'record-outcome':
if (!finder) return 'Opportunity finder not initialized.';
if (restArgs.length < 2) return 'Usage: /opportunities record-outcome <id> <pnl>';
finder.recordOutcome(restArgs[0], {
taken: true,
realizedPnL: parseFloat(restArgs[1]),
closedAt: new Date(),
});
return `Outcome recorded for ${restArgs[0].slice(0, 30)}...`;
case 'stats':
return handleStats(flags.period || restArgs[0]);
case 'history':
return handleStats('30d');
case 'by-platform':
case 'by-type':
return handleStats();
case 'risk':
return handleRisk(restArgs[0]);
case 'estimate':
case 'kelly':
if (!finder) return 'Opportunity finder not initialized.';
if (!restArgs[0]) return `Usage: /opportunities ${command} <id>`;
const opp = finder.get(restArgs[0]);
if (!opp) return `Opportunity ${restArgs[0]} not found.`;
return `Kelly fraction: ${(opp.kellyFraction * 100).toFixed(1)}%\nRecommended size: $${(opp.kellyFraction * 1000).toFixed(2)} per $1000 bankroll`;
case 'auto-match':
return 'Auto-matching requires the embeddings service. Use `/opportunities scan` for standard matching.';
case 'help':
default:
return `**Opportunity Finder Commands**
**Scanning:**
/opportunities scan [query] - Scan for opportunities
/opportunities scan --min-edge 2 - Min 2% edge
/opportunities scan --min-liquidity 1000 - Min $1000 liquidity
/opportunities active [--sort edge|liquidity] - View active
**Real-Time:**
/opportunities realtime start - Start continuous scanning
/opportunities realtime stop - Stop scanning
/opportunities realtime status - Check status
**Market Linking:**
/opportunities link <a> <b> - Link markets
/opportunities unlink <a> <b> - Remove link
/opportunities links - View linked pairs
**Execution:**
/opportunities execute <id> [--size N] - Execute opportunity
/opportunities mark-taken <id> - Mark as taken
/opportunities record-outcome <id> <pnl> - Record P&L
**Analytics:**
/opportunities stats [--period 7d] - Statistics
/opportunities history - Past opportunities
**Risk:**
/opportunities risk <id> - Model risk
/opportunities kelly <id> - Kelly fraction`;
}
}
export default {
name: 'opportunity',
description: 'Find and execute cross-platform arbitrage opportunities across prediction markets',
commands: ['/opportunities', '/opportunity', '/opp'],
handle: execute,
};
Related skills
FAQ
Which platforms does it scan?
Polymarket, Kalshi, Betfair, Smarkets, Manifold, Metaculus, PredictIt, and Drift.
What opportunity types does it find?
Internal (YES+NO<$1), cross-platform, combinatorial, and edge (market vs external model).