Skill: realfishsam/molt-pmxt (version 1.1.0)

--- name: molt-pmxt description: Grants the agent real-time access to prediction markets (Polymarket, Kalshi, Limitless) for fact-checking, probability analysis, and order execution. version: 1.1.0 author: realfishsam license: MIT permissions: - network: ["polymarket.com", "kalshi.com", "limitless.exchange"] - cost: "low" --- # User Guide & System Instructions ## ⚙️ Setup & Configuration To enable trading and full functionality, the following environment variables must be set in the agent's runtime environment: ### Polymarket - `POLYMARKET_PRIVATE_KEY`: Your wallet's private key. - `POLYMARKET_PROXY_ADDRESS`: Proxy wallet address. ### Kalshi - `KALSHI_API_KEY`: Your Kalshi API key. - `KALSHI_PRIVATE_KEY`: Your RSA private key. ### Limitless - `LIMITLESS_API_KEY`: Limitless exchange API key. - `LIMITLESS_PRIVATE_KEY`: Private key for EIP-712 order signing. ## 🧠 Core Capabilities ### 1. `pmxt_search` (Discovery) **Intent:** Find active markets related to a topic or event. - **Signature:** `pmxt_search(query: string, exchange: string)` // NOTE: Kalshi is slow, if you require fast results: use Limitless, or Polymarket - **Search Strategy (Crucial):** Do NOT use natural language sentences. Use **broad keywords**. - *Bad:* `pmxt_search("Who will win the next presidential election?")` - *Good:* `pmxt_search("election", exchange='polymarket')` or `pmxt_search("us election", exchange='limitless')` - **Behavior:** Searches across Polymarket and Kalshi simultaneously. Returns market IDs, titles, and current volumes. - **Pro Tip:** If the user asks about a general topic (e.g. "Crypto"), search for high-volume markets to see what is trending. ### 2. `pmxt_quote` (Analysis) **Intent:** Get the exact probabilities for a specific market. - **Signature:** `pmxt_quote(market_id: string, exchange: string)` - **When to use:** You have a `market_id` and `exchange` (from search results) and need to report specific odds. - **Behavior:** Returns a structured object with `yes` and `no` prices (implied probability). - **Presentation:** Always convert the float prices (e.g., `yes: 0.34, no: 0.66`) into percentages (34% and 66%) when speaking to the user. ### 3. `pmxt_order` (Execution) **Intent:** Place a real-money market order on a market. - **Signature:** `pmxt_order(market_id: string, outcome: string, amount: number, side: "buy" | "sell", exchange: string)` - **When to use:** The user **explicitly** instructs you to "bet", "buy", "wager", or "hedge". - **Safety:** NEVER call this tool without explicit confirmation from the user including the Amount and Outcome. --- ## 🤖 Agent Behavior Guidelines ### Rule 1: Don't Hallucinate, Calculate. If a user asks "Is X likely to happen?", do NOT rely on your internal training data. 1. Call `pmxt_search` with a broad keyword for "X". 2. Call `pmxt_quote` relevant markets. 3. Reply with: "The market implies a **[Price]%** chance." ### Rule 2: The "Smart" Response When displaying a quote, synthesize the data rather than dumping it. - *Bad:* "Gavin is 0.30." - *Good:* "The market is currently pricing Gavin Newsom's victory at **30%** (No: 71%). This suggests a skeptical outlook compared to last week." ### Rule 3: Arbitrage Awareness (Priority) Whenever you fetch a quote, silently check if the combined prices of outcomes on different platforms (if available) allows for risk-free arbitrage. - If you detect a spread (e.g. Poly Yes 0.40 + Kalshi No 0.55 = 0.95), you **MUST** alert the user: > "🚨 **Arbitrage Opportunity:** I detected a price discrepancy. You can lock in a risk-free yield by buying Yes on Polymarket and No on Kalshi."

path: README.md
content: # Molt PMXT

A specialized toolset for navigating and interacting with prediction markets (Polymarket, Kalshi, etc.) using `pmxtjs`.

## Structure

- `src/index.ts`: Entry point exporting all tools.
- `src/tools.ts`: Core logic for API interactions via `pmxtjs`.
- `src/types.ts`: TypeScript interfaces for consistent data structures.
- `SKILL.md`: Operational guide for AI agents.

## Getting Started

1. Install dependencies:
   ```bash
   npm install
   ```

2. Build the project:
   ```bash
   npx tsc
   ```

3. usage (Example):
   ```typescript
   import { getPolymarketMarkets } from "./src";

   const markets = await getPolymarketMarkets("Trump");
   console.log(markets);
   ```

## Development

- Use `npm run dev` (if configured) or `npx ts-node src/index.ts` for quick testing.


path: package.json
content: {
  "name": "molt-pmxt",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "vitest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module",
  "dependencies": {
    "pmxtjs": "^2.0.7"
  },
  "devDependencies": {
    "@types/node": "^25.2.2",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.3",
    "vitest": "^4.0.18"
  }
}

path: src/index.ts
content: export * from "./tools.js";
export * from "./types.js";


path: src/tools.ts
content: import * as pmxt from "pmxtjs";
import { Exchange } from "pmxtjs"; // Import Exchange explicitly for type usage if needed, though we use dynamic discovery
import type { ToolResult } from "./types.js";

// Helper to manage exchange instances dynamically
const exchanges: Map<string, Exchange> = new Map();

// Initialize exchanges by discovering them from the pmxtjs exports
function initExchanges() {
    if (exchanges.size > 0) return;

    // Explicitly configure known exchanges with authentication
    if ((pmxt as any).Polymarket) {
        try {
            exchanges.set('polymarket', new (pmxt as any).Polymarket({
                privateKey: process.env.POLYMARKET_PRIVATE_KEY,
                proxyAddress: process.env.POLYMARKET_PROXY_ADDRESS,
                signatureType: 'gnosis-safe' // Default
            }));
        } catch (e) {
            console.warn('Failed to initialize Polymarket with auth:', e);
        }
    }

    if ((pmxt as any).Kalshi) {
        try {
            exchanges.set('kalshi', new (pmxt as any).Kalshi({
                [REDACTED_SECRET],
                privateKey: process.env.KALSHI_PRIVATE_KEY
            }));
        } catch (e) {
            console.warn('Failed to initialize Kalshi with auth:', e);
        }
    }

    if ((pmxt as any).Limitless) {
        try {
            exchanges.set('limitless', new (pmxt as any).Limitless({
                [REDACTED_SECRET],
                privateKey: process.env.LIMITLESS_PRIVATE_KEY
            }));
        } catch (e) {
            console.warn('Failed to initialize Limitless with auth:', e);
        }
    }

    const exportList = Object.keys(pmxt) as Array<keyof typeof pmxt>;
    // Try to find the Exchange class constructor from exports if needed, or use imported Exchange
    const ExchangeClass = (pmxt as any).Exchange || Exchange;

    // Dynamic discovery for any other exchanges
    for (const key of exportList) {
        const normalizedKey = key.toLowerCase();
        if (exchanges.has(normalizedKey)) continue;

        const ExportedItem = (pmxt as any)[key];

        // Check if it is a class that extends Exchange (and is not Exchange itself)
        // We use prototype check to confirm inheritance
        if (
            typeof ExportedItem === 'function' &&
            ExportedItem.prototype instanceof ExchangeClass &&
            ExportedItem !== ExchangeClass
        ) {
            try {
                const instance = new ExportedItem();
                exchanges.set(normalizedKey, instance);
                // console.log(`Loaded exchange: ${key}`);
            } catch (e) {
                console.warn(`Failed to initialize exchange adapter for ${key}:`, e);
            }
        }
    }
}

// Initialize on module load
initExchanges();

/**
 * Normalizes market data from various exchanges into a common format.
 * Includes specific logic for known exchanges and generic fallbacks for future ones.
 */
function normalizeMarket(m: any, exchangeName: string): any {
    const base = {
        exchange: exchangeName,
        title: m.title || m.question || m.event_ticker || m.name || "Unknown Market",
        description: m.description || m.subtitle || "",
        volume: m.volume || m.volume24h || m.volume_24h || 0,
        resolved: m.closed || m.status === 'closed' || m.resolved || false
    };

    // Specific overrides for ID and other fields where structure varies significantly
    if (exchangeName === 'polymarket') {
        return {
            ...base,
            id: m.yes?.metadata?.clobTokenId || m.marketId || m.id,
            // Polymarket 'volume' is often directly on the object or volume24h
        };
    }

    if (exchangeName === 'kalshi') {
        return {
            ...base,
            id: m.ticker || m.id || m.marketId,
            title: m.title || m.event_ticker, // Kalshi specific prefer event_ticker
            resolved: m.status === "closed"
        };
    }

    if (exchangeName === 'limitless') {
        return {
            ...base,
            id: m.id || m.marketId,
        };
    }

    // Generic fallback for ID
    return {
        ...base,
        id: m.id || m.marketId || m.ticker || m.slug
    };
}

/**
 * Unified search for markets across multiple prediction exchanges.
 * Normalizes results into a common format for the agent.
 */
export async function pmxt_search(query: string, exchange?: string): Promise<ToolResult<any[]>> {
    try {
        const results: any[] = [];
        const normalizedExchange = exchange ? exchange.toLowerCase() : null;

        const targets = normalizedExchange
            ? (exchanges.has(normalizedExchange) ? [normalizedExchange] : [])
            : Array.from(exchanges.keys());

        if (normalizedExchange && targets.length === 0) {
            return { success: false, error: `Unsupported exchange: ${exchange}` };
        }

        const promises = targets.map(async (key) => {
            const client = exchanges.get(key);
            if (!client) return;

            try {
                // Assuming all exchanges implement fetchMarkets({ query })
                // If the signature varies, we might need more robust checking or an adapter
                const markets = await (client as any).fetchMarkets({ query });

                if (Array.isArray(markets)) {
                    results.push(...markets.map(m => normalizeMarket(m, key)));
                }
            } catch (e) {
                console.warn(`Error fetching from ${key}:`, e);
                // Swallow individual exchange errors to allow partial results
            }
        });

        await Promise.all(promises);

        return { success: true, data: results };
    } catch (error: any) {
        return { success: false, error: error.message };
    }
}

/**
 * Get current probabilities for a specific market.
 * dynamically uses the registered exchange client.
 */
export async function pmxt_quote(market_id: string, exchange: string): Promise<ToolResult<any>> {
    try {
        const normalizedExchange = exchange.toLowerCase();
        const client = exchanges.get(normalizedExchange);

        if (!client) {
            throw new Error(`Unsupported exchange: ${exchange}`);
        }

        const orderbook = await client.fetchOrderBook(market_id);

        // Standardize quote extraction
        // Most PMXT exchanges follow the bids/asks array format
        // If a future exchange differs, we might need a normalizeQuote strategy similar to normalizeMarket

        const bestBid = orderbook.bids?.[0]?.price ?? null;
        const bestAsk = orderbook.asks?.[0]?.price ?? null;

        // "Yes" price is the cost to buy a "Yes" share (the best ask)
        // "No" price is the cost to buy a "No" share
        const yes = bestAsk;

        // Handle variations in how "No" price is calculated or represented
        // Kalshi uses 1-100 scale often (or cents), Polymarket 0-1.
        // Assuming client handles basic unit normalization, but logic from original code suggests:
        // Kalshi: 100 - bestBid ? or 1 - bestBid?
        // Original code had: normalizedExchange === "kalshi" ? 100 - bestBid : 1 - bestBid

        let no = bestBid !== null ? 1 - bestBid : null;
        if (normalizedExchange === 'kalshi' && bestBid !== null) {
            // Preserving original logic for Kalshi
            // Check if bestBid > 1 to infer scale? Or stick to original rule
            no = 100 - bestBid;
        }

        return {
            success: true,
            data: {
                yes,
                no,
                // Include raw bestBid/BestAsk for debugging or advanced usage?
                _meta: { bestBid, bestAsk }
            }
        };
    } catch (error: any) {
        return { success: false, error: `Failed to fetch quote: ${error.message}` };
    }
}

/**
 * Place a real-money market order on a market.
 */
export async function pmxt_order(
    market_id: string,
    outcome: string,
    amount: number,
    side: "buy" | "sell",
    exchange: string
): Promise<ToolResult<any>> {
    try {
        const normalizedExchange = exchange.toLowerCase();
        const client = exchanges.get(normalizedExchange);

        if (!client) {
            throw new Error(`Unsupported exchange: ${exchange}`);
        }

        const result = await client.createOrder({
            market_id,
            outcome,
            amount,
            side
        });

        return { success: true, data: result };
    } catch (error: any) {
        return {
            success: false,
            error: `Order failed: ${error.message}. Ensure credentials (API_KEY, PRIVATE_KEY) are correctly configured.`
        };
    }
}


path: src/types.ts
content: export interface Market {
    id: string;
    exchange: string;
    title: string;
    description: string;
    volume: number;
    resolved: boolean;
}

export interface Token {
    id: string;
    symbol: string;
    price: number;
}

export interface OrderbookSnapshot {
    market_id: string;
    bids: Order[];
    asks: Order[];
    timestamp: string;
}

export interface Order {
    price: number;
    size: number;
}

export interface ToolResult<T> {
    success: boolean;
    data?: T;
    error?: string;
}


path: tests/tools.test.ts
content: 
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as pmxt from 'pmxtjs';
import { pmxt_search, pmxt_quote, pmxt_order } from '../src/tools';

// Mock pmxtjs module
vi.mock('pmxtjs', () => {
    // Base Exchange class mock
    class MockExchange {
        fetchMarkets(args?: any): Promise<any[]> { return Promise.resolve([]); }
        fetchOrderBook(id?: string): Promise<any> { return Promise.resolve({ bids: [], asks: [] }); }
        createOrder(args?: any): Promise<any> { return Promise.resolve({ id: 'order-123' }); }
    }

    // Specific Exchange mocks
    class MockPolymarket extends MockExchange {
        fetchMarkets({ query }: { query: string }) {
            return Promise.resolve([
                {
                    id: 'poly-1',
                    question: `Polymarket: ${query}`,
                    volume24h: 1000,
                    closed: false
                }
            ]);
        }

        fetchOrderBook(marketId: string) {
            return Promise.resolve({
                bids: [{ price: 0.4 }],
                asks: [{ price: 0.6 }]
            });
        }
    }

    class MockKalshi extends MockExchange {
        fetchMarkets({ query }: { query: string }) {
            return Promise.resolve([
                {
                    ticker: 'kalshi-1',
                    title: `Kalshi: ${query}`,
                    volume: 500,
                    status: 'active'
                }
            ]);
        }

        fetchOrderBook(marketId: string) {
            // Kalshi logic often involves different price scale, but let's stick to simple mock
            return Promise.resolve({
                bids: [{ price: 40 }], // 40 cents
                asks: [{ price: 60 }]  // 60 cents
            });
        }
    }

    class MockLimitless extends MockExchange {
        fetchMarkets({ query }: { query: string }) {
            // Mock Limitless returns
            return Promise.resolve([
                {
                    id: 'limitless-1',
                    title: `Limitless: ${query}`,
                    volume: 200,
                    resolved: false
                }
            ]);
        }
    }

    return {
        Exchange: MockExchange,
        Polymarket: MockPolymarket,
        Kalshi: MockKalshi,
        Limitless: MockLimitless,
        __esModule: true,
    };
});


describe('pmxt tools', () => {
    beforeEach(() => {
        vi.clearAllMocks();
        // Since tools.ts initializes exchanges on load, we might need to reset module registry 
        // or just rely on the fact that it uses the mocked pmxtjs.
        // The dynamic import in tools.ts happens at top level, so mocks need to be in place before import.
        // Vitest hoist mocks so this should work.
    });

    describe('pmxt_search', () => {
        it('should search across all available exchanges by default', async () => {
            // The tools.ts initializes exchanges on module load.
            // If we mock correctly, it should pick up Polymarket and Kalshi.
            const result = await pmxt_search('election');

            expect(result.success).toBe(true);
            expect(result.data).toHaveLength(3);

            // sort to ensure order doesn't flake
            const sortedData = (result.data as any[]).sort((a, b) => a.exchange.localeCompare(b.exchange));

            expect(sortedData[0].exchange).toBe('kalshi');
            expect(sortedData[0].title).toContain('Kalshi: election');

            expect(sortedData[1].exchange).toBe('limitless');
            expect(sortedData[1].title).toContain('Limitless: election');

            expect(sortedData[2].exchange).toBe('polymarket');
            expect(sortedData[2].title).toContain('Polymarket: election');
        });

        it('should filter by specific exchange', async () => {
            const result = await pmxt_search('election', 'polymarket');

            expect(result.success).toBe(true);
            expect(result.data).toHaveLength(1);
            expect(result.data![0].exchange).toBe('polymarket');
        });

        it('should return error for unsupported exchange', async () => {
            const result = await pmxt_search('election', 'unsupported-exchange');

            expect(result.success).toBe(false);
            expect(result.error).toContain('Unsupported exchange');
        });
    });

    describe('pmxt_quote', () => {
        it('should fetch quote from polymarket', async () => {
            const result = await pmxt_quote('poly-1', 'polymarket');

            expect(result.success).toBe(true);
            // In tools.ts: yes = bestAsk (0.6), no = 1 - bestBid (1 - 0.4 = 0.6)
            expect(result.data.yes).toBe(0.6);
            expect(result.data.no).toBeCloseTo(0.6);
        });

        it('should fetch quote from kalshi', async () => {
            // Kalshi logic in tools.ts: yes = bestAsk (60).
            // no: if bestBid is not null, it's 100 - bestBid (100 - 40 = 60).
            const result = await pmxt_quote('kalshi-1', 'kalshi');

            expect(result.success).toBe(true);
            expect(result.data.yes).toBe(60);
            expect(result.data.no).toBe(60);
        });

        it('should handle missing exchange', async () => {
            const result = await pmxt_quote('id', 'unknown');
            expect(result.success).toBe(false);
        });
    });

    describe('pmxt_order', () => {
        it('should place an order successfully', async () => {
            const result = await pmxt_order('mkt-1', 'yes', 10, 'buy', 'polymarket');

            expect(result.success).toBe(true);
            expect(result.data).toBeDefined();
        });

        it('should fail for unsupported exchange', async () => {
            const result = await pmxt_order('mkt-1', 'yes', 10, 'buy', 'magic-exchange');

            expect(result.success).toBe(false);
            expect(result.error).toContain('Unsupported exchange');
        });
    });
});


path: tsconfig.json
content: {
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    "rootDir": "./src",
    "outDir": "./dist",
    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "nodenext",
    "target": "esnext",
    "types": [
      "node"
    ],
    // For nodejs:
    // "lib": ["esnext"],
    // "types": ["node"],
    // and npm install -D @types/node
    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,
    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    // Style Options
    // "noImplicitReturns": true,
    // "noImplicitOverride": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    // "noFallthroughCasesInSwitch": true,
    // "noPropertyAccessFromIndexSignature": true,
    // Recommended Options
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  },
  "include": [
    "src"
  ]
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)