create-polyclaw-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,570 @@
1
+ import { writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /**
5
+ * Convert agent name to kebab-case directory name
6
+ */
7
+ export function nameToDir(name: string): string {
8
+ return name.toLowerCase().replace(/\s+/g, '-');
9
+ }
10
+
11
+ interface ScaffoldOptions {
12
+ name: string;
13
+ dirName: string;
14
+ apiKey: string;
15
+ apiSecret: string;
16
+ language: 'python' | 'typescript';
17
+ aiProvider: 'anthropic' | 'openai' | 'none';
18
+ aiApiKey?: string;
19
+ }
20
+
21
+ export function scaffoldProject(options: ScaffoldOptions): string {
22
+ const projectDir = join(process.cwd(), options.dirName);
23
+ mkdirSync(projectDir, { recursive: true });
24
+
25
+ // Write .env file with credentials
26
+ const aiKeyLine = options.aiProvider === 'anthropic'
27
+ ? `\nANTHROPIC_API_KEY=${options.aiApiKey || 'your-anthropic-api-key'}\n`
28
+ : options.aiProvider === 'openai'
29
+ ? `\nOPENAI_API_KEY=${options.aiApiKey || 'your-openai-api-key'}\n`
30
+ : '';
31
+
32
+ const envContent = `# Polyclaw Agent: ${options.name}
33
+ # Generated by create-polyclaw-agent
34
+
35
+ POLYCLAW_API_KEY=${options.apiKey}
36
+ POLYCLAW_API_SECRET=${options.apiSecret}
37
+ POLYCLAW_API_URL=https://api.polyclaw.co
38
+ ${aiKeyLine}`;
39
+
40
+ writeFileSync(join(projectDir, '.env'), envContent);
41
+
42
+ if (options.language === 'python') {
43
+ scaffoldPython(projectDir, options);
44
+ } else {
45
+ scaffoldTypeScript(projectDir, options);
46
+ }
47
+
48
+ return projectDir;
49
+ }
50
+
51
+ function scaffoldPython(dir: string, options: ScaffoldOptions) {
52
+ // AI-specific imports and analysis function
53
+ const aiImport = options.aiProvider === 'anthropic'
54
+ ? `import anthropic`
55
+ : options.aiProvider === 'openai'
56
+ ? `import openai`
57
+ : '';
58
+
59
+ const aiKeyEnv = options.aiProvider === 'anthropic'
60
+ ? `AI_API_KEY = os.getenv("ANTHROPIC_API_KEY")`
61
+ : options.aiProvider === 'openai'
62
+ ? `AI_API_KEY = os.getenv("OPENAI_API_KEY")`
63
+ : '';
64
+
65
+ const aiAnalyzeFunc = options.aiProvider === 'anthropic' ? `
66
+ def analyze_markets(markets):
67
+ """Use Claude to analyze prediction markets and pick the best bets."""
68
+ client = anthropic.Anthropic(api_key=AI_API_KEY)
69
+
70
+ # Format markets for the AI
71
+ market_summaries = []
72
+ for i, m in enumerate(markets):
73
+ prices = m.get("outcomePrices", [0.5, 0.5])
74
+ yes_price = float(prices[0]) if prices else 0.5
75
+ no_price = float(prices[1]) if len(prices) > 1 else 1 - yes_price
76
+ market_summaries.append(
77
+ f"{i+1}. {m.get('question', 'Unknown')}\\n"
78
+ f" Yes: {yes_price:.0%} | No: {no_price:.0%} | "
79
+ f"Volume: \${float(m.get('volume', 0)):,.0f} | "
80
+ f"ID: {m.get('id', '')}"
81
+ )
82
+
83
+ markets_text = "\\n".join(market_summaries)
84
+
85
+ response = client.messages.create(
86
+ model="claude-sonnet-4-20250514",
87
+ max_tokens=1024,
88
+ messages=[{
89
+ "role": "user",
90
+ "content": f"""You are a prediction market analyst. Analyze these Polymarket markets and pick up to 3 best betting opportunities.
91
+
92
+ Markets:
93
+ {markets_text}
94
+
95
+ For each pick, respond in this exact JSON format (no markdown, just raw JSON array):
96
+ [
97
+ {{
98
+ "market_index": 1,
99
+ "outcome": "Yes" or "No",
100
+ "side": "BUY",
101
+ "confidence": 0.0 to 1.0,
102
+ "recommended_amount": 5 to 50,
103
+ "reasoning": "One sentence explaining why this is a good bet"
104
+ }}
105
+ ]
106
+
107
+ Rules:
108
+ - Pick 1-3 markets that look like good opportunities
109
+ - Consider the current price, volume, and whether the market seems mispriced
110
+ - Be diverse -- don't pick markets that are all about the same topic
111
+ - Higher confidence = stronger conviction (0.5 = coin flip, 0.9 = very confident)
112
+ - Recommended amount should reflect your confidence (higher confidence = more)
113
+ """
114
+ }]
115
+ )
116
+
117
+ # Parse the AI response
118
+ try:
119
+ picks = json.loads(response.content[0].text)
120
+ model_name = response.model
121
+ return picks, model_name
122
+ except (json.JSONDecodeError, IndexError):
123
+ print(f" AI response: {response.content[0].text}")
124
+ return [], response.model
125
+ ` : options.aiProvider === 'openai' ? `
126
+ def analyze_markets(markets):
127
+ """Use GPT to analyze prediction markets and pick the best bets."""
128
+ client = openai.OpenAI(api_key=AI_API_KEY)
129
+
130
+ # Format markets for the AI
131
+ market_summaries = []
132
+ for i, m in enumerate(markets):
133
+ prices = m.get("outcomePrices", [0.5, 0.5])
134
+ yes_price = float(prices[0]) if prices else 0.5
135
+ no_price = float(prices[1]) if len(prices) > 1 else 1 - yes_price
136
+ market_summaries.append(
137
+ f"{i+1}. {m.get('question', 'Unknown')}\\n"
138
+ f" Yes: {yes_price:.0%} | No: {no_price:.0%} | "
139
+ f"Volume: \${float(m.get('volume', 0)):,.0f} | "
140
+ f"ID: {m.get('id', '')}"
141
+ )
142
+
143
+ markets_text = "\\n".join(market_summaries)
144
+
145
+ response = client.chat.completions.create(
146
+ model="gpt-4o",
147
+ messages=[{
148
+ "role": "user",
149
+ "content": f"""You are a prediction market analyst. Analyze these Polymarket markets and pick up to 3 best betting opportunities.
150
+
151
+ Markets:
152
+ {markets_text}
153
+
154
+ For each pick, respond in this exact JSON format (no markdown, just raw JSON array):
155
+ [
156
+ {{
157
+ "market_index": 1,
158
+ "outcome": "Yes" or "No",
159
+ "side": "BUY",
160
+ "confidence": 0.0 to 1.0,
161
+ "recommended_amount": 5 to 50,
162
+ "reasoning": "One sentence explaining why this is a good bet"
163
+ }}
164
+ ]
165
+
166
+ Rules:
167
+ - Pick 1-3 markets that look like good opportunities
168
+ - Consider the current price, volume, and whether the market seems mispriced
169
+ - Be diverse -- don't pick markets that are all about the same topic
170
+ - Higher confidence = stronger conviction (0.5 = coin flip, 0.9 = very confident)
171
+ - Recommended amount should reflect your confidence (higher confidence = more)
172
+ """
173
+ }],
174
+ )
175
+
176
+ # Parse the AI response
177
+ try:
178
+ picks = json.loads(response.choices[0].message.content)
179
+ model_name = response.model
180
+ return picks, model_name
181
+ except (json.JSONDecodeError, IndexError):
182
+ print(f" AI response: {response.choices[0].message.content}")
183
+ return [], response.model
184
+ ` : `
185
+ def analyze_markets(markets):
186
+ """Placeholder: Add your own AI logic here to analyze markets."""
187
+ # This is where you connect your AI model.
188
+ # Return a list of picks in this format:
189
+ # [{"market_index": 1, "outcome": "Yes", "side": "BUY",
190
+ # "confidence": 0.7, "recommended_amount": 10,
191
+ # "reasoning": "Your analysis here"}]
192
+ #
193
+ # Example with OpenAI:
194
+ # client = openai.OpenAI()
195
+ # response = client.chat.completions.create(model="gpt-4o", ...)
196
+ #
197
+ # Example with Anthropic:
198
+ # client = anthropic.Anthropic()
199
+ # response = client.messages.create(model="claude-sonnet-4-20250514", ...)
200
+
201
+ print(" No AI configured. Add your API key to .env and update bot.py")
202
+ print(" See https://polyclaw.co/docs for examples.")
203
+ return [], "none"
204
+ `;
205
+
206
+ const botPy = `"""
207
+ ${options.name} - Polyclaw AI Trading Agent
208
+
209
+ This bot connects to Polyclaw, fetches prediction markets from Polymarket,
210
+ uses AI to analyze them, and submits trade proposals for you to review.
211
+
212
+ Usage:
213
+ python3 -m venv venv && source venv/bin/activate
214
+ pip install -r requirements.txt
215
+ python bot.py
216
+ """
217
+
218
+ import os
219
+ import json
220
+ import time
221
+ import hmac
222
+ import hashlib
223
+ import requests
224
+ from dotenv import load_dotenv
225
+ ${aiImport}
226
+
227
+ load_dotenv()
228
+
229
+ API_KEY = os.getenv("POLYCLAW_API_KEY")
230
+ API_SECRET = os.getenv("POLYCLAW_API_SECRET")
231
+ BASE_URL = os.getenv("POLYCLAW_API_URL", "https://api.polyclaw.co")
232
+ ${aiKeyEnv}
233
+
234
+
235
+ class PolyclawClient:
236
+ def __init__(self, api_key: str, api_secret: str, base_url: str):
237
+ self.api_key = api_key
238
+ self.base_url = base_url
239
+ self.signing_key = hmac.new(
240
+ b"polyclaw-secret-salt", api_secret.encode(), hashlib.sha256
241
+ ).hexdigest()
242
+
243
+ def _sign(self, timestamp: str, method: str, path: str, body: str = "") -> str:
244
+ message = f"{timestamp}{method.upper()}{path}{body}"
245
+ return hmac.new(
246
+ self.signing_key.encode(), message.encode(), hashlib.sha256
247
+ ).hexdigest()
248
+
249
+ def _request(self, method: str, path: str, body: dict = None):
250
+ timestamp = str(int(time.time() * 1000))
251
+ body_str = json.dumps(body) if body else ""
252
+ signature = self._sign(timestamp, method, path, body_str)
253
+ headers = {
254
+ "Content-Type": "application/json",
255
+ "X-Polyclaw-Api-Key": self.api_key,
256
+ "X-Polyclaw-Signature": signature,
257
+ "X-Polyclaw-Timestamp": timestamp,
258
+ }
259
+ url = f"{self.base_url}{path}"
260
+ response = requests.request(method, url, headers=headers, json=body)
261
+ return response.json()
262
+
263
+ def get_markets(self, page: int = 1, limit: int = 20):
264
+ return self._request("GET", f"/api/markets?page={page}&limit={limit}")
265
+
266
+ def submit_proposal(self, market_id, outcome, side, recommended_amount, confidence, reasoning=None, model=None):
267
+ body = {"marketId": market_id, "outcome": outcome, "side": side,
268
+ "recommendedAmount": recommended_amount, "confidence": confidence}
269
+ if reasoning: body["reasoning"] = reasoning
270
+ if model: body["model"] = model
271
+ return self._request("POST", "/api/proposals", body)
272
+
273
+ ${aiAnalyzeFunc}
274
+
275
+ def main():
276
+ if not API_KEY or not API_SECRET:
277
+ print("Error: POLYCLAW_API_KEY and POLYCLAW_API_SECRET must be set in .env")
278
+ return
279
+ ${options.aiProvider !== 'none' ? `
280
+ ai_key_name = "${options.aiProvider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'}"
281
+ if not AI_API_KEY:
282
+ print(f"Error: {ai_key_name} must be set in .env")
283
+ return
284
+ ` : ''}
285
+ client = PolyclawClient(API_KEY, API_SECRET, BASE_URL)
286
+ print(f"${options.name} is online!")
287
+ print()
288
+
289
+ # Fetch markets
290
+ print("Fetching markets from Polymarket...")
291
+ result = client.get_markets(limit=20)
292
+
293
+ if not result.get("success"):
294
+ print(f"Error: {result.get('error', 'Unknown error')}")
295
+ return
296
+
297
+ markets = result.get("data", [])
298
+ print(f"Found {len(markets)} markets")
299
+ print()
300
+
301
+ # Ask AI to analyze and pick the best bets
302
+ print("Asking AI to analyze markets...\\n")
303
+ picks, model_name = analyze_markets(markets)
304
+
305
+ if not picks:
306
+ print("No picks from AI. Try again later.")
307
+ return
308
+
309
+ print(f" AI picked {len(picks)} market(s) (model: {model_name})\\n")
310
+
311
+ # Submit each pick as a proposal
312
+ for pick in picks:
313
+ idx = pick.get("market_index", 1) - 1
314
+ if idx < 0 or idx >= len(markets):
315
+ continue
316
+
317
+ market = markets[idx]
318
+ print(f" -> {market.get('question', 'Unknown')}")
319
+ print(f" {pick['side']} {pick['outcome']} | Confidence: {pick['confidence']:.0%} | Amount: \${pick['recommended_amount']}")
320
+ print(f" Reasoning: {pick.get('reasoning', 'N/A')}")
321
+
322
+ proposal = client.submit_proposal(
323
+ market_id=market["id"],
324
+ outcome=pick["outcome"],
325
+ side=pick["side"],
326
+ recommended_amount=pick["recommended_amount"],
327
+ confidence=pick["confidence"],
328
+ reasoning=pick.get("reasoning"),
329
+ model=model_name,
330
+ )
331
+
332
+ if proposal.get("success"):
333
+ print(f" Submitted!\\n")
334
+ else:
335
+ print(f" Error: {proposal.get('error', 'Unknown')}\\n")
336
+
337
+ print("Done! Check your dashboard to review proposals.")
338
+
339
+
340
+ if __name__ == "__main__":
341
+ main()
342
+ `;
343
+
344
+ const aiDep = options.aiProvider === 'anthropic'
345
+ ? 'anthropic>=0.40.0\n'
346
+ : options.aiProvider === 'openai'
347
+ ? 'openai>=1.50.0\n'
348
+ : '';
349
+
350
+ const requirements = `requests>=2.31.0
351
+ python-dotenv>=1.0.0
352
+ ${aiDep}`;
353
+
354
+ const gitignore = `.env
355
+ __pycache__/
356
+ *.pyc
357
+ .venv/
358
+ `;
359
+
360
+ writeFileSync(join(dir, 'bot.py'), botPy);
361
+ writeFileSync(join(dir, 'requirements.txt'), requirements);
362
+ writeFileSync(join(dir, '.gitignore'), gitignore);
363
+ }
364
+
365
+ function scaffoldTypeScript(dir: string, options: ScaffoldOptions) {
366
+ const botTs = `/**
367
+ * ${options.name} - Polyclaw AI Trading Agent
368
+ *
369
+ * This bot connects to Polyclaw to browse prediction markets and submit
370
+ * trade proposals. A human reviews and approves proposals before execution.
371
+ *
372
+ * Usage:
373
+ * npm install
374
+ * npx tsx bot.ts
375
+ */
376
+
377
+ import { createHmac } from 'crypto';
378
+ import 'dotenv/config';
379
+
380
+ const API_KEY = process.env.POLYCLAW_API_KEY!;
381
+ const API_SECRET = process.env.POLYCLAW_API_SECRET!;
382
+ const BASE_URL = process.env.POLYCLAW_API_URL || 'https://api.polyclaw.co';
383
+
384
+ class PolyclawClient {
385
+ private apiKey: string;
386
+ private signingKey: string;
387
+ private baseUrl: string;
388
+
389
+ constructor(config: { apiKey: string; apiSecret: string; baseUrl: string }) {
390
+ this.apiKey = config.apiKey;
391
+ this.baseUrl = config.baseUrl;
392
+ this.signingKey = createHmac('sha256', 'polyclaw-secret-salt')
393
+ .update(config.apiSecret)
394
+ .digest('hex');
395
+ }
396
+
397
+ private sign(timestamp: string, method: string, path: string, body?: string): string {
398
+ const message = \`\${timestamp}\${method.toUpperCase()}\${path}\${body || ''}\`;
399
+ return createHmac('sha256', this.signingKey).update(message).digest('hex');
400
+ }
401
+
402
+ private async request<T>(method: string, path: string, body?: object): Promise<T> {
403
+ const timestamp = Date.now().toString();
404
+ const bodyStr = body ? JSON.stringify(body) : '';
405
+ const signature = this.sign(timestamp, method, path, bodyStr);
406
+ const response = await fetch(\`\${this.baseUrl}\${path}\`, {
407
+ method,
408
+ headers: {
409
+ 'Content-Type': 'application/json',
410
+ 'X-Polyclaw-Api-Key': this.apiKey,
411
+ 'X-Polyclaw-Signature': signature,
412
+ 'X-Polyclaw-Timestamp': timestamp,
413
+ },
414
+ body: body ? bodyStr : undefined,
415
+ });
416
+ return response.json() as T;
417
+ }
418
+
419
+ async getMarkets(page = 1, limit = 20) {
420
+ return this.request<any>('GET', \`/api/markets?page=\${page}&limit=\${limit}\`);
421
+ }
422
+
423
+ async submitProposal(proposal: {
424
+ marketId: string;
425
+ outcome: string;
426
+ side: string;
427
+ recommendedAmount: number;
428
+ confidence: number;
429
+ reasoning?: string;
430
+ model?: string;
431
+ }) {
432
+ return this.request<any>('POST', '/api/proposals', proposal);
433
+ }
434
+ }
435
+
436
+ interface MarketPlay {
437
+ market: any;
438
+ outcome: string;
439
+ side: string;
440
+ confidence: number;
441
+ reasoning: string;
442
+ score: number;
443
+ }
444
+
445
+ function findBestPlay(markets: any[]): MarketPlay | null {
446
+ // Analyze markets and find the best opportunity.
447
+ // Strategy: Look for markets with strong price signals and high volume.
448
+ const scored: MarketPlay[] = [];
449
+
450
+ for (const market of markets) {
451
+ const yesPrice = parseFloat(market.outcomePrices?.[0] ?? '0.5');
452
+ const volume = parseFloat(market.volume ?? '0');
453
+
454
+ // Skip markets that are basically decided
455
+ if (yesPrice < 0.05 || yesPrice > 0.95) continue;
456
+
457
+ const edge = Math.abs(yesPrice - 0.5);
458
+ const liquidityScore = Math.min(volume / 500000, 1.0);
459
+ const score = edge * 0.6 + liquidityScore * 0.4;
460
+
461
+ let outcome: string, side: string, confidence: number, reasoning: string;
462
+
463
+ if (yesPrice < 0.40) {
464
+ outcome = 'Yes'; side = 'BUY';
465
+ confidence = Math.round((0.5 + edge) * 100) / 100;
466
+ reasoning = \`Yes is priced at \${(yesPrice * 100).toFixed(0)}% -- looks undervalued with $\${volume.toLocaleString()} volume\`;
467
+ } else if (yesPrice > 0.60) {
468
+ outcome = 'Yes'; side = 'BUY';
469
+ confidence = Math.round((0.5 + edge * 0.8) * 100) / 100;
470
+ reasoning = \`Market strongly favors Yes at \${(yesPrice * 100).toFixed(0)}% -- riding the momentum\`;
471
+ } else {
472
+ outcome = 'Yes'; side = 'BUY';
473
+ confidence = Math.round((0.5 + edge * 0.5) * 100) / 100;
474
+ reasoning = \`Market is contested at \${(yesPrice * 100).toFixed(0)}% -- taking a position\`;
475
+ }
476
+
477
+ scored.push({ market, outcome, side, confidence: Math.min(confidence, 0.95), reasoning, score });
478
+ }
479
+
480
+ scored.sort((a, b) => b.score - a.score);
481
+ return scored[0] || null;
482
+ }
483
+
484
+ async function main() {
485
+ if (!API_KEY || !API_SECRET) {
486
+ console.error('Error: POLYCLAW_API_KEY and POLYCLAW_API_SECRET must be set in .env');
487
+ return;
488
+ }
489
+
490
+ const client = new PolyclawClient({ apiKey: API_KEY, apiSecret: API_SECRET, baseUrl: BASE_URL });
491
+ console.log('${options.name} is online!');
492
+ console.log();
493
+
494
+ // Fetch markets
495
+ console.log('Scanning markets...');
496
+ const result = await client.getMarkets(1, 20);
497
+
498
+ if (!result.success) {
499
+ console.error(\`Error: \${result.error || 'Unknown error'}\`);
500
+ return;
501
+ }
502
+
503
+ const markets = result.data || [];
504
+ console.log(\`Analyzing \${markets.length} markets...\\n\`);
505
+
506
+ // Find the best play
507
+ const best = findBestPlay(markets);
508
+
509
+ if (!best) {
510
+ console.log('No good opportunities found right now.');
511
+ return;
512
+ }
513
+
514
+ const yesPrice = parseFloat(best.market.outcomePrices?.[0] ?? '0.5');
515
+ console.log(' Best opportunity found:');
516
+ console.log(\` \${best.market.question}\`);
517
+ console.log(\` Yes: \${(yesPrice * 100).toFixed(0)}% | Volume: $\${parseFloat(best.market.volume || '0').toLocaleString()}\`);
518
+ console.log(\` Play: \${best.side} \${best.outcome} | Confidence: \${(best.confidence * 100).toFixed(0)}%\`);
519
+ console.log(\` Reasoning: \${best.reasoning}\`);
520
+ console.log();
521
+
522
+ // Submit proposal
523
+ // Tip: Replace this logic with your own AI model for smarter predictions!
524
+ // const aiResponse = await openai.chat.completions.create({ model: 'gpt-4o', ... });
525
+ // const modelName = aiResponse.model; // e.g. "gpt-4o-2025-01-15"
526
+ console.log('Submitting proposal...');
527
+ const proposal = await client.submitProposal({
528
+ marketId: best.market.id,
529
+ outcome: best.outcome,
530
+ side: best.side,
531
+ recommendedAmount: 10.0,
532
+ confidence: best.confidence,
533
+ reasoning: best.reasoning,
534
+ model: 'your-model-here', // Replace with your AI model name
535
+ });
536
+
537
+ if (proposal.success) {
538
+ console.log('Proposal submitted! Check your dashboard to approve it.');
539
+ } else {
540
+ console.error(\`Proposal error: \${proposal.error || 'Unknown'}\`);
541
+ }
542
+ }
543
+
544
+ main();
545
+ `;
546
+
547
+ const packageJson = `{
548
+ "name": "${options.dirName}",
549
+ "version": "0.1.0",
550
+ "private": true,
551
+ "type": "module",
552
+ "scripts": {
553
+ "start": "npx tsx bot.ts"
554
+ },
555
+ "dependencies": {
556
+ "dotenv": "^16.4.0",
557
+ "tsx": "^4.7.0"
558
+ }
559
+ }
560
+ `;
561
+
562
+ const gitignore = `.env
563
+ node_modules/
564
+ dist/
565
+ `;
566
+
567
+ writeFileSync(join(dir, 'bot.ts'), botTs);
568
+ writeFileSync(join(dir, 'package.json'), packageJson);
569
+ writeFileSync(join(dir, '.gitignore'), gitignore);
570
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "lib": ["ES2022"],
7
+ "noUnusedLocals": false,
8
+ "noUnusedParameters": false
9
+ },
10
+ "include": ["src/**/*"]
11
+ }