agentlaunch-templates 0.4.2 → 0.4.4

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.
Files changed (51) hide show
  1. package/dist/__tests__/build.test.d.ts +1 -1
  2. package/dist/__tests__/build.test.js +5 -5
  3. package/dist/__tests__/build.test.js.map +1 -1
  4. package/dist/__tests__/consumer-commerce.test.d.ts +11 -0
  5. package/dist/__tests__/consumer-commerce.test.d.ts.map +1 -0
  6. package/dist/__tests__/consumer-commerce.test.js +118 -0
  7. package/dist/__tests__/consumer-commerce.test.js.map +1 -0
  8. package/dist/__tests__/swarm-starter-integration.test.d.ts +12 -0
  9. package/dist/__tests__/swarm-starter-integration.test.d.ts.map +1 -0
  10. package/dist/__tests__/swarm-starter-integration.test.js +143 -0
  11. package/dist/__tests__/swarm-starter-integration.test.js.map +1 -0
  12. package/dist/__tests__/swarm-starter.test.d.ts +16 -0
  13. package/dist/__tests__/swarm-starter.test.d.ts.map +1 -0
  14. package/dist/__tests__/swarm-starter.test.js +310 -0
  15. package/dist/__tests__/swarm-starter.test.js.map +1 -0
  16. package/dist/claude-context.d.ts +1 -1
  17. package/dist/claude-context.d.ts.map +1 -1
  18. package/dist/claude-context.js +55 -49
  19. package/dist/claude-context.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/people.d.ts +108 -0
  25. package/dist/people.d.ts.map +1 -0
  26. package/dist/people.js +563 -0
  27. package/dist/people.js.map +1 -0
  28. package/dist/presets.d.ts +13 -13
  29. package/dist/presets.d.ts.map +1 -1
  30. package/dist/presets.js +331 -96
  31. package/dist/presets.js.map +1 -1
  32. package/dist/registry.d.ts +3 -8
  33. package/dist/registry.d.ts.map +1 -1
  34. package/dist/registry.js +8 -28
  35. package/dist/registry.js.map +1 -1
  36. package/dist/templates/chat-memory.d.ts +5 -14
  37. package/dist/templates/chat-memory.d.ts.map +1 -1
  38. package/dist/templates/chat-memory.js +142 -220
  39. package/dist/templates/chat-memory.js.map +1 -1
  40. package/dist/templates/consumer-commerce.d.ts +14 -0
  41. package/dist/templates/consumer-commerce.d.ts.map +1 -0
  42. package/dist/templates/consumer-commerce.js +439 -0
  43. package/dist/templates/consumer-commerce.js.map +1 -0
  44. package/dist/templates/genesis.d.ts.map +1 -1
  45. package/dist/templates/genesis.js +10 -0
  46. package/dist/templates/genesis.js.map +1 -1
  47. package/dist/templates/swarm-starter.d.ts +26 -0
  48. package/dist/templates/swarm-starter.d.ts.map +1 -0
  49. package/dist/templates/swarm-starter.js +1421 -0
  50. package/dist/templates/swarm-starter.js.map +1 -0
  51. package/package.json +3 -2
@@ -0,0 +1,1421 @@
1
+ /**
2
+ * swarm-starter template: full commerce stack for agent swarms
3
+ *
4
+ * This is the flagship template for the AgentLaunch platform. It generates a
5
+ * production-ready agent with an inline commerce engine: payments, pricing,
6
+ * tiers, revenue tracking, self-awareness (own token metrics), and cross-holdings.
7
+ *
8
+ * Layers (bottom to top):
9
+ * 1. Logger — structured logging with audit trail
10
+ * 2. Security — rate limiting, input validation
11
+ * 3. Health — uptime, error rate tracking
12
+ * 4. Cache — in-memory TTL cache
13
+ * 5. Revenue/Tier — PricingTable, TierManager
14
+ * 6. Commerce — PaymentService, WalletManager, RevenueTracker
15
+ * 7. SelfAware — token price/holder awareness
16
+ * 8. Holdings — HoldingsManager for cross-token operations
17
+ * 9. SwarmBusiness — YOUR LOGIC HERE
18
+ *
19
+ * Platform constants (source of truth: deployed smart contracts):
20
+ * - Deploy fee: 120 FET (read dynamically, can change via multi-sig)
21
+ * - Graduation target: 30,000 FET -> auto DEX listing
22
+ * - Trading fee: 2% -> 100% to protocol treasury (NO creator fee)
23
+ */
24
+ // Environment-based URL resolution (production is default, set AGENT_LAUNCH_ENV=dev for dev)
25
+ const PROD_API_URL = 'https://agent-launch.ai/api';
26
+ const DEV_API_URL = 'https://launchpad-backend-dev-1056182620041.us-central1.run.app';
27
+ const RESOLVED_API_URL = process.env.AGENT_LAUNCH_API_URL ??
28
+ (process.env.AGENT_LAUNCH_ENV === 'dev' ? DEV_API_URL : PROD_API_URL);
29
+ export const template = {
30
+ name: "swarm-starter",
31
+ description: "Full commerce stack for agent swarms — payments, tiers, revenue, self-awareness, cross-holdings",
32
+ category: "Swarm",
33
+ variables: [
34
+ { name: "agent_name", required: true, description: "Name of the agent" },
35
+ {
36
+ name: "description",
37
+ default: "A swarm-starter agent",
38
+ description: "Short description of what this agent does",
39
+ },
40
+ {
41
+ name: "role",
42
+ default: "custom",
43
+ description: "Agent role: oracle, brain, analyst, coordinator, sentinel, launcher, scout, or custom",
44
+ },
45
+ {
46
+ name: "service_price_afet",
47
+ default: "1000000000000000",
48
+ description: "Price per service call in atestfet (default 0.001 FET)",
49
+ },
50
+ {
51
+ name: "interval_seconds",
52
+ default: "300",
53
+ description: "Background task interval in seconds",
54
+ },
55
+ {
56
+ name: "token_address",
57
+ default: "",
58
+ description: "Own token contract address for self-awareness (empty to disable)",
59
+ },
60
+ {
61
+ name: "premium_token_threshold",
62
+ default: "1000",
63
+ description: "Tokens needed for premium tier access",
64
+ },
65
+ {
66
+ name: "effort_mode",
67
+ default: "normal",
68
+ description: "Initial effort mode: normal, boost, or conserve",
69
+ },
70
+ {
71
+ name: "rate_limit_per_minute",
72
+ default: "10",
73
+ description: "Max requests per user per minute",
74
+ },
75
+ {
76
+ name: "free_requests_per_day",
77
+ default: "10",
78
+ description: "Free-tier daily request limit",
79
+ },
80
+ ],
81
+ dependencies: ["requests", "web3"],
82
+ secrets: [
83
+ "AGENTVERSE_API_KEY",
84
+ "AGENTLAUNCH_API_KEY",
85
+ "ASI1_API_KEY",
86
+ "AGENT_ADDRESS",
87
+ "AGENT_OWNER_ADDRESS",
88
+ "BSC_PRIVATE_KEY",
89
+ ],
90
+ code: `#!/usr/bin/env python3
91
+ """
92
+ {{agent_name}} — Swarm Agent (role: {{role}})
93
+
94
+ {{description}}
95
+
96
+ Generated by: agentlaunch scaffold {{agent_name}} --type swarm-starter
97
+
98
+ Commerce layers (inline):
99
+ 1. Logger — structured logging with audit trail
100
+ 2. Security — rate limiting, input validation
101
+ 3. Health — uptime, error rate tracking
102
+ 4. Cache — in-memory TTL cache
103
+ 5. Revenue/Tier — PricingTable, TierManager (token-gated access)
104
+ 6. Commerce — PaymentService, WalletManager, RevenueTracker
105
+ 7. SelfAware — own token price/holder awareness
106
+ 8. Holdings — cross-token operations via web3 or handoff links
107
+ 9. Memory+LLM — conversation history + ASI1-mini reasoning
108
+ 10. SwarmBusiness — YOUR LOGIC HERE
109
+
110
+ Platform constants (source of truth: deployed smart contracts):
111
+ - Deploy fee: 120 FET (read dynamically, can change via multi-sig)
112
+ - Graduation target: 30,000 FET -> auto DEX listing
113
+ - Trading fee: 2% -> 100% to protocol treasury (NO creator fee)
114
+ """
115
+
116
+ from uagents import Agent, Context, Protocol, Model
117
+ from uagents_core.contrib.protocols.chat import (
118
+ ChatAcknowledgement,
119
+ ChatMessage,
120
+ EndSessionContent,
121
+ TextContent,
122
+ chat_protocol_spec,
123
+ )
124
+
125
+ import json
126
+ import os
127
+ import time
128
+ from collections import defaultdict
129
+ from datetime import datetime, date
130
+ from typing import Any, Dict, List, Optional, Tuple
131
+ from uuid import uuid4
132
+
133
+ import requests
134
+ from openai import OpenAI
135
+
136
+ # ==========================================================================
137
+ # COM-01: Payment Protocol — try official, fallback to custom models
138
+ # ==========================================================================
139
+
140
+ try:
141
+ from uagents_core.contrib.protocols.payment import (
142
+ RequestPayment,
143
+ CommitPayment,
144
+ CompletePayment,
145
+ RejectPayment,
146
+ CancelPayment,
147
+ Funds,
148
+ payment_protocol_spec,
149
+ )
150
+ PAYMENT_PROTOCOL_AVAILABLE = True
151
+ except ImportError:
152
+ PAYMENT_PROTOCOL_AVAILABLE = False
153
+
154
+ class Funds(Model):
155
+ denom: str = "atestfet"
156
+ amount: int = 0
157
+
158
+ class RequestPayment(Model):
159
+ request_id: str = ""
160
+ amount: int = 0
161
+ denom: str = "atestfet"
162
+ service: str = ""
163
+ recipient: str = ""
164
+
165
+ class CommitPayment(Model):
166
+ request_id: str = ""
167
+ tx_hash: str = ""
168
+
169
+ class CompletePayment(Model):
170
+ request_id: str = ""
171
+ result: str = ""
172
+
173
+ class RejectPayment(Model):
174
+ request_id: str = ""
175
+ reason: str = ""
176
+
177
+ class CancelPayment(Model):
178
+ request_id: str = ""
179
+ reason: str = ""
180
+
181
+ payment_protocol_spec = None
182
+
183
+
184
+ # ==========================================================================
185
+ # API CONFIG
186
+ # ==========================================================================
187
+
188
+ AGENTLAUNCH_API = os.environ.get("AGENTLAUNCH_API", "${RESOLVED_API_URL}")
189
+ OWNER_ADDRESS = os.environ.get("AGENT_OWNER_ADDRESS", "")
190
+ TOKEN_ADDRESS = os.environ.get("TOKEN_ADDRESS", "{{token_address}}")
191
+
192
+ BUSINESS = {
193
+ "name": "{{agent_name}}",
194
+ "description": "{{description}}",
195
+ "role": "{{role}}",
196
+ "version": "1.0.0",
197
+ "service_price_afet": int("{{service_price_afet}}"),
198
+ "interval_seconds": int("{{interval_seconds}}"),
199
+ "premium_token_threshold": int("{{premium_token_threshold}}"),
200
+ "effort_mode": "{{effort_mode}}",
201
+ "rate_limit_per_minute": int("{{rate_limit_per_minute}}"),
202
+ "free_requests_per_day": int("{{free_requests_per_day}}"),
203
+ "max_input_length": 5000,
204
+ }
205
+
206
+ # FETAgentCoin contract ABI — the token IS the bonding curve
207
+ TOKEN_ABI = [
208
+ {"name": "buyTokens", "type": "function",
209
+ "inputs": [{"name": "buyer", "type": "address"},
210
+ {"name": "slippageAmount", "type": "uint256"},
211
+ {"name": "_buyAmount", "type": "uint256"}],
212
+ "outputs": [], "stateMutability": "nonpayable"},
213
+ {"name": "sellTokens", "type": "function",
214
+ "inputs": [{"name": "tokenAmount", "type": "uint256"}],
215
+ "outputs": [], "stateMutability": "nonpayable"},
216
+ {"name": "FET_TOKEN", "type": "function",
217
+ "inputs": [], "outputs": [{"name": "", "type": "address"}],
218
+ "stateMutability": "view"},
219
+ {"name": "calculateTokensReceived", "type": "function",
220
+ "inputs": [{"name": "fetAmount", "type": "uint256"}],
221
+ "outputs": [{"name": "", "type": "uint256"}],
222
+ "stateMutability": "view"},
223
+ ]
224
+
225
+ ERC20_ABI = [
226
+ {"name": "approve", "type": "function",
227
+ "inputs": [{"name": "spender", "type": "address"},
228
+ {"name": "amount", "type": "uint256"}],
229
+ "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable"},
230
+ {"name": "allowance", "type": "function",
231
+ "inputs": [{"name": "owner", "type": "address"},
232
+ {"name": "spender", "type": "address"}],
233
+ "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view"},
234
+ {"name": "balanceOf", "type": "function",
235
+ "inputs": [{"name": "account", "type": "address"}],
236
+ "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view"},
237
+ {"name": "decimals", "type": "function",
238
+ "inputs": [], "outputs": [{"name": "", "type": "uint8"}],
239
+ "stateMutability": "view"},
240
+ {"name": "transfer", "type": "function",
241
+ "inputs": [{"name": "to", "type": "address"},
242
+ {"name": "value", "type": "uint256"}],
243
+ "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable"},
244
+ ]
245
+
246
+ # Default TFET address on BSC Testnet
247
+ TFET_ADDRESS = "0x304ddf3eE068c53514f782e2341B71A80c8aE3C7"
248
+
249
+
250
+ # ==========================================================================
251
+ # LAYER 1: LOGGER — structured logging with audit trail
252
+ # ==========================================================================
253
+
254
+
255
+ class Logger:
256
+ @staticmethod
257
+ def info(ctx: Context, event: str, data: Optional[Dict] = None) -> None:
258
+ ctx.logger.info(f"[{event}] {json.dumps(data or {})}")
259
+
260
+ @staticmethod
261
+ def audit(ctx: Context, user: str, action: str) -> None:
262
+ ctx.logger.info(
263
+ f"[AUDIT] user={user[:20]} action={action} "
264
+ f"ts={datetime.now().isoformat()}"
265
+ )
266
+
267
+ @staticmethod
268
+ def error(ctx: Context, event: str, error: str) -> None:
269
+ ctx.logger.error(f"[{event}] {error}")
270
+
271
+
272
+ # ==========================================================================
273
+ # LAYER 2: SECURITY — rate limiting, input validation
274
+ # ==========================================================================
275
+
276
+
277
+ class Security:
278
+ def __init__(self) -> None:
279
+ self._requests: Dict[str, List[float]] = defaultdict(list)
280
+ self._check_count: int = 0
281
+
282
+ def check(self, ctx: Context, user_id: str, message: str) -> Tuple[Optional[str], Optional[str]]:
283
+ now = time.time()
284
+
285
+ # Sliding window rate limit
286
+ self._requests[user_id] = [
287
+ t for t in self._requests[user_id] if now - t < 60
288
+ ]
289
+ if len(self._requests[user_id]) >= BUSINESS["rate_limit_per_minute"]:
290
+ return None, "Rate limit exceeded. Please wait a moment."
291
+ self._requests[user_id].append(now)
292
+
293
+ # Periodic cleanup
294
+ self._check_count += 1
295
+ if self._check_count % 100 == 0:
296
+ stale = [
297
+ k for k, v in self._requests.items()
298
+ if not v or (now - max(v)) > 300
299
+ ]
300
+ for k in stale:
301
+ del self._requests[k]
302
+
303
+ # Input validation
304
+ if not message or not message.strip():
305
+ return None, "Empty message."
306
+ if len(message) > BUSINESS["max_input_length"]:
307
+ return None, f"Message too long (max {BUSINESS['max_input_length']} chars)."
308
+
309
+ return message.strip(), None
310
+
311
+
312
+ # ==========================================================================
313
+ # LAYER 3: HEALTH — uptime, error rate tracking
314
+ # ==========================================================================
315
+
316
+
317
+ class Health:
318
+ def __init__(self) -> None:
319
+ self._start: datetime = datetime.now()
320
+ self._requests: int = 0
321
+ self._errors: int = 0
322
+
323
+ def record(self, success: bool) -> None:
324
+ self._requests += 1
325
+ if not success:
326
+ self._errors += 1
327
+
328
+ def status(self) -> Dict[str, Any]:
329
+ uptime = (datetime.now() - self._start).total_seconds()
330
+ error_rate = (self._errors / self._requests * 100) if self._requests else 0
331
+ return {
332
+ "status": "healthy" if error_rate < 10 else "degraded",
333
+ "uptime_seconds": int(uptime),
334
+ "requests": self._requests,
335
+ "error_rate": f"{error_rate:.1f}%",
336
+ }
337
+
338
+
339
+ # ==========================================================================
340
+ # LAYER 4: CACHE — in-memory TTL cache
341
+ # ==========================================================================
342
+
343
+
344
+ class Cache:
345
+ def __init__(self, max_size: int = 1000) -> None:
346
+ self._data: Dict[str, tuple] = {}
347
+ self._max_size: int = max_size
348
+
349
+ def get(self, key: str) -> Any:
350
+ if key in self._data:
351
+ value, expires = self._data[key]
352
+ if expires > time.time():
353
+ return value
354
+ del self._data[key]
355
+ return None
356
+
357
+ def set(self, key: str, value: Any, ttl: int = 300) -> None:
358
+ if len(self._data) >= self._max_size:
359
+ now = time.time()
360
+ expired = [k for k, (_, exp) in self._data.items() if exp <= now]
361
+ for k in expired:
362
+ del self._data[k]
363
+ if len(self._data) >= self._max_size:
364
+ to_drop = sorted(self._data.items(), key=lambda x: x[1][1])[
365
+ : self._max_size // 10
366
+ ]
367
+ for k, _ in to_drop:
368
+ del self._data[k]
369
+ self._data[key] = (value, time.time() + ttl)
370
+
371
+
372
+ # ==========================================================================
373
+ # LAYER 5: REVENUE/TIER — PricingTable + TierManager (COM-03)
374
+ # ==========================================================================
375
+
376
+
377
+ class PricingTable:
378
+ """Per-service pricing stored in ctx.storage."""
379
+
380
+ STORAGE_KEY = "pricing_table"
381
+
382
+ def get_price(self, ctx: Context, service: str) -> int:
383
+ table = json.loads(ctx.storage.get(self.STORAGE_KEY) or "{}")
384
+ return table.get(service, BUSINESS["service_price_afet"])
385
+
386
+ def set_price(self, ctx: Context, service: str, amount_afet: int) -> None:
387
+ table = json.loads(ctx.storage.get(self.STORAGE_KEY) or "{}")
388
+ table[service] = amount_afet
389
+ ctx.storage.set(self.STORAGE_KEY, json.dumps(table))
390
+
391
+ def list_services(self, ctx: Context) -> Dict[str, int]:
392
+ table = json.loads(ctx.storage.get(self.STORAGE_KEY) or "{}")
393
+ if not table:
394
+ table["default"] = BUSINESS["service_price_afet"]
395
+ return table
396
+
397
+
398
+ class TierManager:
399
+ """Checks token holdings via AgentLaunch API. Returns free or premium."""
400
+
401
+ def __init__(self, cache: Cache) -> None:
402
+ self._cache = cache
403
+
404
+ def get_tier(self, sender: str) -> str:
405
+ if not TOKEN_ADDRESS:
406
+ return "free"
407
+ cached = self._cache.get(f"tier:{sender}")
408
+ if cached is not None:
409
+ return cached
410
+ try:
411
+ r = requests.get(
412
+ f"{AGENTLAUNCH_API}/agents/token/{TOKEN_ADDRESS}/holders",
413
+ timeout=5,
414
+ )
415
+ if r.status_code == 200:
416
+ holders = r.json() if isinstance(r.json(), list) else r.json().get("holders", [])
417
+ for h in holders:
418
+ addr = h.get("address", h.get("holder", ""))
419
+ if addr.lower() == sender.lower():
420
+ balance = int(h.get("balance", h.get("amount", 0)))
421
+ tier = "premium" if balance >= BUSINESS["premium_token_threshold"] else "free"
422
+ self._cache.set(f"tier:{sender}", tier, ttl=300)
423
+ return tier
424
+ except Exception:
425
+ pass
426
+ self._cache.set(f"tier:{sender}", "free", ttl=300)
427
+ return "free"
428
+
429
+ def check_quota(self, user_id: str, tier: str, usage: Dict) -> Tuple[bool, Optional[str]]:
430
+ today = date.today().isoformat()
431
+ user_usage = usage.get(user_id, [])
432
+ user_usage = [t for t in user_usage if t.startswith(today)]
433
+ usage[user_id] = user_usage
434
+ today_count = len(user_usage)
435
+ limit = 10000 if tier == "premium" else BUSINESS["free_requests_per_day"]
436
+ if today_count >= limit:
437
+ if tier == "free":
438
+ return False, (
439
+ f"Free limit reached ({limit}/day). "
440
+ f"Hold {BUSINESS['premium_token_threshold']} tokens for premium!"
441
+ )
442
+ return False, f"Daily limit reached ({limit}/day)."
443
+ user_usage.append(datetime.now().isoformat())
444
+ usage[user_id] = user_usage
445
+ return True, None
446
+
447
+
448
+ # ==========================================================================
449
+ # LAYER 6: COMMERCE — PaymentService, WalletManager, RevenueTracker (COM-02/04)
450
+ # ==========================================================================
451
+
452
+
453
+ class PaymentService:
454
+ """Seller-side payment handling. Tracks pending requests and transaction log."""
455
+
456
+ def __init__(self, pricing: PricingTable) -> None:
457
+ self._pricing = pricing
458
+ self._pending: Dict[str, dict] = {}
459
+
460
+ async def charge(
461
+ self, ctx: Context, sender: str, amount_afet: int, service: str
462
+ ) -> str:
463
+ request_id = str(uuid4())
464
+ self._pending[request_id] = {
465
+ "sender": sender,
466
+ "amount": amount_afet,
467
+ "service": service,
468
+ "ts": datetime.now().isoformat(),
469
+ }
470
+ msg = RequestPayment(
471
+ request_id=request_id,
472
+ amount=amount_afet,
473
+ denom="atestfet",
474
+ service=service,
475
+ recipient=str(agent.wallet.address()),
476
+ )
477
+ await ctx.send(sender, msg)
478
+ Logger.info(ctx, "CHARGE_SENT", {"to": sender[:20], "amount": amount_afet, "service": service})
479
+ return request_id
480
+
481
+ async def on_commit(self, ctx: Context, sender: str, msg) -> Optional[dict]:
482
+ request_id = getattr(msg, "request_id", "")
483
+ if request_id not in self._pending:
484
+ Logger.error(ctx, "UNKNOWN_PAYMENT", f"request_id={request_id}")
485
+ return None
486
+ pending = self._pending.pop(request_id)
487
+ # Log the transaction
488
+ tx_log = json.loads(ctx.storage.get("tx_log") or "[]")
489
+ tx_log.append({
490
+ "type": "income",
491
+ "request_id": request_id,
492
+ "sender": sender[:20],
493
+ "amount": pending["amount"],
494
+ "service": pending["service"],
495
+ "tx_hash": getattr(msg, "tx_hash", ""),
496
+ "ts": datetime.now().isoformat(),
497
+ })
498
+ ctx.storage.set("tx_log", json.dumps(tx_log[-1000:]))
499
+ Logger.info(ctx, "PAYMENT_RECEIVED", {"from": sender[:20], "amount": pending["amount"]})
500
+ return pending
501
+
502
+ def get_balance(self, ctx: Context) -> int:
503
+ try:
504
+ return int(ctx.ledger.query_bank_balance(
505
+ str(agent.wallet.address()), "atestfet"
506
+ ))
507
+ except Exception:
508
+ pass
509
+ return 0
510
+
511
+
512
+ class WalletManager:
513
+ """Balance queries and fund alerts."""
514
+
515
+ def get_balance(self, ctx: Context) -> int:
516
+ try:
517
+ return int(ctx.ledger.query_bank_balance(
518
+ str(agent.wallet.address()), "atestfet"
519
+ ))
520
+ except Exception:
521
+ pass
522
+ return 0
523
+
524
+ def get_address(self, ctx: Context) -> str:
525
+ try:
526
+ return str(agent.wallet.address())
527
+ except Exception:
528
+ pass
529
+ return ""
530
+
531
+ def fund_check(self, ctx: Context, min_balance: int = 10_000_000_000_000_000) -> bool:
532
+ balance = self.get_balance(ctx)
533
+ if balance < min_balance:
534
+ Logger.info(ctx, "LOW_FUNDS", {
535
+ "balance": balance,
536
+ "min_required": min_balance,
537
+ "deficit": min_balance - balance,
538
+ })
539
+ return False
540
+ return True
541
+
542
+
543
+ class RevenueTracker:
544
+ """Income/expense log in ctx.storage."""
545
+
546
+ STORAGE_KEY = "revenue_log"
547
+
548
+ def record_income(
549
+ self, ctx: Context, amount: int, source: str, service: str
550
+ ) -> None:
551
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
552
+ log.append({
553
+ "type": "income",
554
+ "amount": amount,
555
+ "source": source[:20],
556
+ "service": service,
557
+ "ts": datetime.now().isoformat(),
558
+ })
559
+ ctx.storage.set(self.STORAGE_KEY, json.dumps(log[-5000:]))
560
+
561
+ def record_expense(
562
+ self, ctx: Context, amount: int, dest: str, service: str
563
+ ) -> None:
564
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
565
+ log.append({
566
+ "type": "expense",
567
+ "amount": amount,
568
+ "dest": dest[:20],
569
+ "service": service,
570
+ "ts": datetime.now().isoformat(),
571
+ })
572
+ ctx.storage.set(self.STORAGE_KEY, json.dumps(log[-5000:]))
573
+
574
+ def get_summary(self, ctx: Context) -> Dict[str, Any]:
575
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
576
+ total_income = sum(e["amount"] for e in log if e["type"] == "income")
577
+ total_expense = sum(e["amount"] for e in log if e["type"] == "expense")
578
+ return {
579
+ "total_income_afet": total_income,
580
+ "total_expense_afet": total_expense,
581
+ "net_afet": total_income - total_expense,
582
+ "tx_count": len(log),
583
+ }
584
+
585
+ def get_daily_summary(self, ctx: Context) -> Dict[str, Any]:
586
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
587
+ today = date.today().isoformat()
588
+ today_entries = [e for e in log if e["ts"].startswith(today)]
589
+ income = sum(e["amount"] for e in today_entries if e["type"] == "income")
590
+ expense = sum(e["amount"] for e in today_entries if e["type"] == "expense")
591
+ return {
592
+ "date": today,
593
+ "income_afet": income,
594
+ "expense_afet": expense,
595
+ "net_afet": income - expense,
596
+ "tx_count": len(today_entries),
597
+ }
598
+
599
+
600
+ # ==========================================================================
601
+ # LAYER 7: SELF-AWARE — own token price/holder awareness (COM-05)
602
+ # ==========================================================================
603
+
604
+
605
+ class SelfAwareMixin:
606
+ """Reads own token price + holders from AgentLaunch API."""
607
+
608
+ HISTORY_KEY = "price_history"
609
+
610
+ def __init__(self, cache: Cache) -> None:
611
+ self._cache = cache
612
+ self._price: float = 0.0
613
+ self._holders: int = 0
614
+ self._market_cap: float = 0.0
615
+ self._ma_7d: float = 0.0
616
+ self._effort_mode: str = BUSINESS["effort_mode"]
617
+
618
+ def update(self, ctx: Context) -> None:
619
+ if not TOKEN_ADDRESS:
620
+ return
621
+ cached = self._cache.get("self_aware_data")
622
+ if cached:
623
+ self._price, self._holders, self._market_cap = cached
624
+ self._update_effort()
625
+ return
626
+ try:
627
+ r = requests.get(
628
+ f"{AGENTLAUNCH_API}/tokens/address/{TOKEN_ADDRESS}",
629
+ timeout=5,
630
+ )
631
+ if r.status_code == 200:
632
+ data = r.json()
633
+ token = data if "price" in data else data.get("token", data.get("data", {}))
634
+ self._price = float(token.get("price", token.get("currentPrice", 0)))
635
+ self._holders = int(token.get("holders", token.get("holderCount", 0)))
636
+ self._market_cap = float(token.get("marketCap", token.get("market_cap", 0)))
637
+ self._cache.set("self_aware_data", (self._price, self._holders, self._market_cap), ttl=60)
638
+
639
+ # Store price history (up to 30 days of hourly samples)
640
+ history = json.loads(ctx.storage.get(self.HISTORY_KEY) or "[]")
641
+ history.append({
642
+ "price": self._price,
643
+ "holders": self._holders,
644
+ "ts": datetime.now().isoformat(),
645
+ })
646
+ history = history[-720:] # 30 days * 24 hours
647
+ ctx.storage.set(self.HISTORY_KEY, json.dumps(history))
648
+
649
+ # Calculate 7-day moving average
650
+ recent = history[-168:] # 7 days * 24 hours
651
+ if recent:
652
+ self._ma_7d = sum(p["price"] for p in recent) / len(recent)
653
+
654
+ self._update_effort()
655
+ Logger.info(ctx, "SELF_AWARE_UPDATE", {
656
+ "price": self._price,
657
+ "holders": self._holders,
658
+ "ma_7d": round(self._ma_7d, 8),
659
+ "effort": self._effort_mode,
660
+ })
661
+ except Exception as e:
662
+ Logger.error(ctx, "SELF_AWARE_FETCH", str(e))
663
+
664
+ def _update_effort(self) -> None:
665
+ if self._ma_7d <= 0:
666
+ self._effort_mode = BUSINESS["effort_mode"]
667
+ return
668
+ ratio = self._price / self._ma_7d if self._ma_7d > 0 else 1.0
669
+ if ratio > 1.1:
670
+ self._effort_mode = "boost"
671
+ elif ratio < 0.9:
672
+ self._effort_mode = "conserve"
673
+ else:
674
+ self._effort_mode = "normal"
675
+
676
+ def get_effort_mode(self) -> str:
677
+ return self._effort_mode
678
+
679
+ def get_token_summary(self) -> Dict[str, Any]:
680
+ return {
681
+ "token_address": TOKEN_ADDRESS,
682
+ "price": self._price,
683
+ "holders": self._holders,
684
+ "market_cap": self._market_cap,
685
+ "ma_7d": round(self._ma_7d, 8),
686
+ "effort_mode": self._effort_mode,
687
+ }
688
+
689
+
690
+ # ==========================================================================
691
+ # LAYER 8: CROSS-HOLDINGS — HoldingsManager (COM-06)
692
+ # ==========================================================================
693
+
694
+
695
+ class HoldingsManager:
696
+ """
697
+ Direct on-chain token operations using web3 + eth_account.
698
+ Fallback: generate handoff links for human signing.
699
+ """
700
+
701
+ BSC_RPC = os.environ.get("BSC_RPC", "https://data-seed-prebsc-1-s1.binance.org:8545")
702
+ FRONTEND_URL = os.environ.get("AGENTLAUNCH_FRONTEND", "https://agent-launch.ai")
703
+
704
+ def buy_via_web3(
705
+ self, ctx: Context, token_address: str, fet_amount_wei: int,
706
+ slippage_percent: int = 5,
707
+ ) -> Tuple[bool, str]:
708
+ """
709
+ Buy tokens on the bonding curve. The token IS the bonding curve.
710
+
711
+ Flow: approve FET -> call buyTokens(buyer, minTokens, fetAmount) on token.
712
+ """
713
+ try:
714
+ from web3 import Web3
715
+
716
+ private_key = os.environ.get("BSC_PRIVATE_KEY", "")
717
+ if not private_key:
718
+ return False, self.generate_buy_link(token_address, fet_amount_wei)
719
+
720
+ w3 = Web3(Web3.HTTPProvider(self.BSC_RPC))
721
+ if not w3.is_connected():
722
+ return False, "Cannot connect to BSC RPC."
723
+
724
+ account = w3.eth.account.from_key(private_key)
725
+ token_addr = Web3.to_checksum_address(token_address)
726
+
727
+ token_contract = w3.eth.contract(address=token_addr, abi=TOKEN_ABI)
728
+
729
+ # 1. Resolve FET token address from contract (fallback to testnet TFET)
730
+ try:
731
+ fet_addr = token_contract.functions.FET_TOKEN().call()
732
+ except Exception:
733
+ fet_addr = TFET_ADDRESS
734
+ fet_contract = w3.eth.contract(
735
+ address=Web3.to_checksum_address(fet_addr), abi=ERC20_ABI,
736
+ )
737
+
738
+ # 2. Check FET balance
739
+ balance = fet_contract.functions.balanceOf(account.address).call()
740
+ if balance < fet_amount_wei:
741
+ return False, (
742
+ f"Insufficient FET. Have {balance}, need {fet_amount_wei}. "
743
+ f"Deficit: {fet_amount_wei - balance}"
744
+ )
745
+
746
+ nonce = w3.eth.get_transaction_count(account.address)
747
+ gas_price = w3.eth.gas_price
748
+ chain_id = w3.eth.chain_id
749
+
750
+ # 3. Approve FET spend on the token contract (if needed)
751
+ allowance = fet_contract.functions.allowance(
752
+ account.address, token_addr,
753
+ ).call()
754
+ if allowance < fet_amount_wei:
755
+ approve_tx = fet_contract.functions.approve(
756
+ token_addr, fet_amount_wei,
757
+ ).build_transaction({
758
+ "from": account.address, "nonce": nonce,
759
+ "gas": 100000, "gasPrice": gas_price, "chainId": chain_id,
760
+ })
761
+ signed_approve = w3.eth.account.sign_transaction(approve_tx, private_key)
762
+ approve_hash = w3.eth.send_raw_transaction(signed_approve.raw_transaction)
763
+ w3.eth.wait_for_transaction_receipt(approve_hash, timeout=60)
764
+ nonce += 1
765
+ Logger.info(ctx, "FET_APPROVED", {
766
+ "spender": token_address[:12], "amount": fet_amount_wei,
767
+ })
768
+
769
+ # 4. Calculate expected tokens and apply slippage
770
+ expected_tokens = token_contract.functions.calculateTokensReceived(
771
+ fet_amount_wei,
772
+ ).call()
773
+ min_tokens = expected_tokens * (100 - slippage_percent) // 100
774
+
775
+ # 5. Buy: buyTokens(buyer, slippageAmount, _buyAmount)
776
+ buy_tx = token_contract.functions.buyTokens(
777
+ account.address, min_tokens, fet_amount_wei,
778
+ ).build_transaction({
779
+ "from": account.address, "nonce": nonce,
780
+ "gas": 300000, "gasPrice": gas_price, "chainId": chain_id,
781
+ })
782
+ signed_buy = w3.eth.account.sign_transaction(buy_tx, private_key)
783
+ tx_hash = w3.eth.send_raw_transaction(signed_buy.raw_transaction)
784
+
785
+ # 6. Wait for confirmation
786
+ receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
787
+ if receipt.status != 1:
788
+ return False, f"Buy transaction reverted: {tx_hash.hex()}"
789
+
790
+ Logger.info(ctx, "BUY_VIA_WEB3", {
791
+ "token": token_address[:12],
792
+ "fet_spent": fet_amount_wei,
793
+ "expected_tokens": expected_tokens,
794
+ "min_tokens": min_tokens,
795
+ "tx": tx_hash.hex(),
796
+ })
797
+ return True, f"0x{tx_hash.hex()}"
798
+
799
+ except ImportError:
800
+ return False, self.generate_buy_link(token_address, fet_amount_wei)
801
+ except Exception as e:
802
+ Logger.error(ctx, "BUY_ERROR", str(e))
803
+ return False, str(e)
804
+
805
+ def sell_via_web3(
806
+ self, ctx: Context, token_address: str, token_amount: int,
807
+ ) -> Tuple[bool, str]:
808
+ """
809
+ Sell tokens back to the bonding curve. No approval needed — the token
810
+ contract burns from msg.sender directly.
811
+
812
+ Flow: call sellTokens(tokenAmount) on the token contract.
813
+ """
814
+ try:
815
+ from web3 import Web3
816
+
817
+ private_key = os.environ.get("BSC_PRIVATE_KEY", "")
818
+ if not private_key:
819
+ return False, self.generate_sell_link(token_address, token_amount)
820
+
821
+ w3 = Web3(Web3.HTTPProvider(self.BSC_RPC))
822
+ if not w3.is_connected():
823
+ return False, "Cannot connect to BSC RPC."
824
+
825
+ account = w3.eth.account.from_key(private_key)
826
+ token_addr = Web3.to_checksum_address(token_address)
827
+
828
+ # 1. Check token balance
829
+ token_contract = w3.eth.contract(address=token_addr, abi=TOKEN_ABI + ERC20_ABI)
830
+ balance = token_contract.functions.balanceOf(account.address).call()
831
+ if balance < token_amount:
832
+ return False, (
833
+ f"Insufficient tokens. Have {balance}, want to sell {token_amount}."
834
+ )
835
+
836
+ # 2. Sell: sellTokens(tokenAmount) — no approval needed
837
+ nonce = w3.eth.get_transaction_count(account.address)
838
+ sell_tx = token_contract.functions.sellTokens(
839
+ token_amount,
840
+ ).build_transaction({
841
+ "from": account.address, "nonce": nonce,
842
+ "gas": 300000, "gasPrice": w3.eth.gas_price,
843
+ "chainId": w3.eth.chain_id,
844
+ })
845
+ signed_sell = w3.eth.account.sign_transaction(sell_tx, private_key)
846
+ tx_hash = w3.eth.send_raw_transaction(signed_sell.raw_transaction)
847
+
848
+ # 3. Wait for confirmation
849
+ receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=60)
850
+ if receipt.status != 1:
851
+ return False, f"Sell transaction reverted: {tx_hash.hex()}"
852
+
853
+ Logger.info(ctx, "SELL_VIA_WEB3", {
854
+ "token": token_address[:12],
855
+ "amount": token_amount,
856
+ "tx": tx_hash.hex(),
857
+ })
858
+ return True, f"0x{tx_hash.hex()}"
859
+
860
+ except ImportError:
861
+ return False, self.generate_sell_link(token_address, token_amount)
862
+ except Exception as e:
863
+ Logger.error(ctx, "SELL_ERROR", str(e))
864
+ return False, str(e)
865
+
866
+ def get_balances(
867
+ self, ctx: Context, token_address: str = "",
868
+ ) -> Dict[str, Any]:
869
+ """Check wallet BNB, FET, and (optionally) token balances."""
870
+ result: Dict[str, Any] = {"bnb": 0, "fet": 0, "token": 0, "wallet": ""}
871
+ try:
872
+ from web3 import Web3
873
+
874
+ private_key = os.environ.get("BSC_PRIVATE_KEY", "")
875
+ if not private_key:
876
+ return result
877
+
878
+ w3 = Web3(Web3.HTTPProvider(self.BSC_RPC))
879
+ if not w3.is_connected():
880
+ return result
881
+
882
+ account = w3.eth.account.from_key(private_key)
883
+ result["wallet"] = account.address
884
+
885
+ # BNB balance
886
+ result["bnb"] = w3.eth.get_balance(account.address)
887
+
888
+ # FET balance
889
+ try:
890
+ if token_address:
891
+ tc = w3.eth.contract(
892
+ address=Web3.to_checksum_address(token_address), abi=TOKEN_ABI,
893
+ )
894
+ fet_addr = tc.functions.FET_TOKEN().call()
895
+ else:
896
+ fet_addr = TFET_ADDRESS
897
+ fet_contract = w3.eth.contract(
898
+ address=Web3.to_checksum_address(fet_addr), abi=ERC20_ABI,
899
+ )
900
+ result["fet"] = fet_contract.functions.balanceOf(account.address).call()
901
+ except Exception:
902
+ pass
903
+
904
+ # Token balance (if address provided)
905
+ if token_address:
906
+ try:
907
+ token_contract = w3.eth.contract(
908
+ address=Web3.to_checksum_address(token_address), abi=ERC20_ABI,
909
+ )
910
+ result["token"] = token_contract.functions.balanceOf(account.address).call()
911
+ except Exception:
912
+ pass
913
+
914
+ except ImportError:
915
+ pass
916
+ except Exception:
917
+ pass
918
+ return result
919
+
920
+ def get_holdings_summary(self, ctx: Context, token_addresses: List[str]) -> List[Dict]:
921
+ results = []
922
+ for addr in token_addresses:
923
+ try:
924
+ r = requests.get(
925
+ f"{AGENTLAUNCH_API}/tokens/address/{addr}",
926
+ timeout=5,
927
+ )
928
+ if r.status_code == 200:
929
+ data = r.json()
930
+ token = data if "name" in data else data.get("token", data.get("data", {}))
931
+ results.append({
932
+ "address": addr,
933
+ "name": token.get("name", "Unknown"),
934
+ "price": float(token.get("price", token.get("currentPrice", 0))),
935
+ "holders": int(token.get("holders", token.get("holderCount", 0))),
936
+ })
937
+ except Exception:
938
+ results.append({"address": addr, "name": "Error", "price": 0, "holders": 0})
939
+ return results
940
+
941
+ @staticmethod
942
+ def _validate_eth_address(address: str) -> bool:
943
+ """Validate Ethereum address format to prevent URL injection."""
944
+ import re
945
+ return bool(re.match(r'^0x[a-fA-F0-9]{40}$', address))
946
+
947
+ @staticmethod
948
+ def generate_buy_link(token_address: str, amount: int = 0) -> str:
949
+ if not HoldingsManager._validate_eth_address(token_address):
950
+ return f"Error: Invalid token address format: {token_address}"
951
+ base = os.environ.get("AGENTLAUNCH_FRONTEND", "https://agent-launch.ai")
952
+ url = f"{base}/trade/{token_address}?action=buy"
953
+ if amount:
954
+ url += f"&amount={amount}"
955
+ return f"Sign here: {url}"
956
+
957
+ @staticmethod
958
+ def generate_sell_link(token_address: str, amount: int = 0) -> str:
959
+ if not HoldingsManager._validate_eth_address(token_address):
960
+ return f"Error: Invalid token address format: {token_address}"
961
+ base = os.environ.get("AGENTLAUNCH_FRONTEND", "https://agent-launch.ai")
962
+ url = f"{base}/trade/{token_address}?action=sell"
963
+ if amount:
964
+ url += f"&amount={amount}"
965
+ return f"Sign here: {url}"
966
+
967
+
968
+ # ==========================================================================
969
+ # AGENTLAUNCH INTEGRATION — tokenization via API
970
+ # ==========================================================================
971
+
972
+
973
+ class AgentLaunch:
974
+ @staticmethod
975
+ def tokenize() -> Dict:
976
+ agent_address = os.environ.get("AGENT_ADDRESS")
977
+ if not agent_address:
978
+ return {"error": "AGENT_ADDRESS env var not set."}
979
+ try:
980
+ r = requests.post(
981
+ f"{AGENTLAUNCH_API}/agents/tokenize",
982
+ headers={
983
+ "X-API-Key": os.environ.get("AGENTLAUNCH_API_KEY", ""),
984
+ "Content-Type": "application/json",
985
+ },
986
+ json={
987
+ "agentAddress": agent_address,
988
+ "name": BUSINESS["name"],
989
+ "description": BUSINESS["description"],
990
+ },
991
+ timeout=30,
992
+ )
993
+ return r.json() if r.status_code in [200, 201] else {"error": r.text}
994
+ except Exception as e:
995
+ return {"error": str(e)}
996
+
997
+
998
+ # ==========================================================================
999
+ # REPLY HELPER
1000
+ # ==========================================================================
1001
+
1002
+
1003
+ async def reply(ctx: Context, sender: str, text: str, end: bool = False) -> None:
1004
+ content = [TextContent(type="text", text=text)]
1005
+ if end:
1006
+ content.append(EndSessionContent(type="end-session"))
1007
+ try:
1008
+ await ctx.send(
1009
+ sender,
1010
+ ChatMessage(timestamp=datetime.now(), msg_id=uuid4(), content=content),
1011
+ )
1012
+ except Exception as e:
1013
+ ctx.logger.error(f"Failed to send reply to {sender[:20]}: {e}")
1014
+
1015
+
1016
+ # ==========================================================================
1017
+ # LAYER 9: SWARM BUSINESS — YOUR LOGIC HERE
1018
+ # ==========================================================================
1019
+ #
1020
+ # Everything above is the swarm commerce stack. Below is where you add
1021
+ # your agent's unique intelligence. The commerce layers handle payments,
1022
+ # tiers, and revenue. Focus on what your agent DOES, not how it gets paid.
1023
+ #
1024
+ # The default handler below routes standard commands (help, status, tokenize)
1025
+ # and delegates business queries to the handle_business() function.
1026
+ #
1027
+ # Examples of what to add:
1028
+ #
1029
+ # async def handle_business(ctx, sender, message, tier):
1030
+ # # Your agent's core logic goes here
1031
+ # return "result"
1032
+ #
1033
+ # @agent.on_interval(period=BUSINESS["interval_seconds"])
1034
+ # async def background_task(ctx):
1035
+ # # Periodic work: monitoring, data collection, alerts
1036
+ # pass
1037
+
1038
+
1039
+ # ==========================================================================
1040
+ # LAYER 9: CONVERSATION MEMORY + LLM
1041
+ # ==========================================================================
1042
+
1043
+
1044
+ class ConversationMemory:
1045
+ """Per-user conversation history with persistence."""
1046
+
1047
+ def __init__(self, max_messages: int = 20):
1048
+ self._max = max_messages
1049
+ self._history: Dict[str, List[Dict[str, str]]] = defaultdict(list)
1050
+
1051
+ def add(self, user_id: str, role: str, content: str) -> None:
1052
+ self._history[user_id].append({"role": role, "content": content})
1053
+ if len(self._history[user_id]) > self._max:
1054
+ self._history[user_id] = self._history[user_id][-self._max:]
1055
+
1056
+ def get(self, user_id: str) -> List[Dict[str, str]]:
1057
+ return list(self._history[user_id])
1058
+
1059
+ def clear(self, user_id: str) -> None:
1060
+ self._history[user_id] = []
1061
+
1062
+ def save(self, ctx: Context) -> None:
1063
+ ctx.storage.set("conv_memory", json.dumps(dict(self._history)))
1064
+
1065
+ def load(self, ctx: Context) -> None:
1066
+ raw = ctx.storage.get("conv_memory")
1067
+ if raw:
1068
+ try:
1069
+ data = json.loads(raw)
1070
+ for k, v in data.items():
1071
+ self._history[k] = v[-self._max:]
1072
+ except (json.JSONDecodeError, TypeError):
1073
+ pass
1074
+
1075
+
1076
+ # ASI1-mini LLM client (OpenAI-compatible)
1077
+ ASI1_API_KEY = os.environ.get("ASI1_API_KEY", "")
1078
+ _llm_client = None
1079
+
1080
+ def get_llm_client() -> Optional[OpenAI]:
1081
+ global _llm_client
1082
+ if _llm_client is None and ASI1_API_KEY:
1083
+ _llm_client = OpenAI(base_url="https://api.asi1.ai/v1", api_key=ASI1_API_KEY)
1084
+ return _llm_client
1085
+
1086
+ SYSTEM_PROMPT = (
1087
+ f"You are {BUSINESS['name']}, an AI agent in the ASI Alliance swarm. "
1088
+ f"Role: {BUSINESS['role']}. {BUSINESS['description']}. "
1089
+ f"You have a built-in commerce stack: you can charge for services, "
1090
+ f"track revenue, and trade tokens. Be helpful, specific, and concise. "
1091
+ f"When asked about services, quote prices. When asked to perform a "
1092
+ f"service, do your best and explain your reasoning."
1093
+ )
1094
+
1095
+ memory = ConversationMemory()
1096
+
1097
+
1098
+ def llm_respond(user_message: str, sender: str, tier: str) -> Optional[str]:
1099
+ """Generate an LLM response with conversation context."""
1100
+ client = get_llm_client()
1101
+ if not client:
1102
+ return None
1103
+
1104
+ history = memory.get(sender)
1105
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
1106
+ messages.extend(history[-10:]) # Last 10 messages for context
1107
+ messages.append({"role": "user", "content": user_message})
1108
+
1109
+ try:
1110
+ resp = client.chat.completions.create(
1111
+ model="asi1-mini",
1112
+ messages=messages,
1113
+ max_tokens=1000,
1114
+ temperature=0.7,
1115
+ )
1116
+ return resp.choices[0].message.content
1117
+ except Exception as e:
1118
+ return f"[LLM unavailable: {str(e)[:100]}] I can still help with commands: help, services, revenue, token, balance"
1119
+
1120
+
1121
+ async def handle_business(
1122
+ ctx: Context, sender: str, message: str, tier: str
1123
+ ) -> str:
1124
+ """
1125
+ ===================================================================
1126
+ YOUR SWARM LOGIC — This is where your agent becomes unique
1127
+ ===================================================================
1128
+
1129
+ This function handles all incoming messages. Commerce commands are
1130
+ handled directly. Everything else goes to the LLM (ASI1-mini) with
1131
+ full conversation memory.
1132
+
1133
+ Available objects:
1134
+ pricing — PricingTable: get_price(), set_price(), list_services()
1135
+ tier_mgr — TierManager: get_tier(), check_quota()
1136
+ payments — PaymentService: charge(), on_commit(), get_balance()
1137
+ wallet — WalletManager: get_balance(), get_address(), fund_check()
1138
+ revenue — RevenueTracker: record_income/expense(), get_summary()
1139
+ self_aware — SelfAwareMixin: update(), get_effort_mode(), get_token_summary()
1140
+ holdings — HoldingsManager: buy_via_web3(), sell_via_web3()
1141
+ cache — Cache: get(), set()
1142
+ memory — ConversationMemory: add(), get(), clear(), save(), load()
1143
+ """
1144
+ lower = message.lower()
1145
+
1146
+ # --- Commerce commands (handled directly, no LLM needed) ---
1147
+
1148
+ if lower.startswith("price "):
1149
+ parts = message.split(maxsplit=1)
1150
+ if len(parts) > 1:
1151
+ services = pricing.list_services(ctx)
1152
+ svc = parts[1].strip()
1153
+ if svc in services:
1154
+ price = services[svc]
1155
+ return f"Service '{svc}' costs {price} atestfet ({price / 1e18:.6f} FET)"
1156
+ return f"Unknown service '{svc}'. Available: {', '.join(services.keys())}"
1157
+ return "Usage: price <service_name>"
1158
+
1159
+ if lower in ("services", "pricing", "menu"):
1160
+ services = pricing.list_services(ctx)
1161
+ lines = [f"Available services for {BUSINESS['name']}:"]
1162
+ for svc, price in services.items():
1163
+ lines.append(f" {svc}: {price} atestfet ({price / 1e18:.6f} FET)")
1164
+ lines.append(f"\\nYour tier: {tier.upper()}")
1165
+ return "\\n".join(lines)
1166
+
1167
+ if lower == "revenue" and (not OWNER_ADDRESS or sender == OWNER_ADDRESS):
1168
+ summary = revenue.get_summary(ctx)
1169
+ daily = revenue.get_daily_summary(ctx)
1170
+ return (
1171
+ f"Revenue Summary:\\n"
1172
+ f" All-time income: {summary['total_income_afet']} atestfet\\n"
1173
+ f" All-time expense: {summary['total_expense_afet']} atestfet\\n"
1174
+ f" Net: {summary['net_afet']} atestfet\\n"
1175
+ f" Transactions: {summary['tx_count']}\\n\\n"
1176
+ f"Today ({daily['date']}):\\n"
1177
+ f" Income: {daily['income_afet']} atestfet\\n"
1178
+ f" Expense: {daily['expense_afet']} atestfet"
1179
+ )
1180
+
1181
+ if lower in ("token", "self", "awareness"):
1182
+ if TOKEN_ADDRESS:
1183
+ s = self_aware.get_token_summary()
1184
+ return (
1185
+ f"Token Status:\\n"
1186
+ f" Address: {s['token_address'][:16]}...\\n"
1187
+ f" Price: {s['price']:.8f} FET\\n"
1188
+ f" Holders: {s['holders']}\\n"
1189
+ f" 7d MA: {s['ma_7d']:.8f} FET\\n"
1190
+ f" Market Cap: {s.get('market_cap', 0):.2f} FET\\n"
1191
+ f" Effort Mode: {s['effort_mode'].upper()}"
1192
+ )
1193
+ return "No token address configured. Set TOKEN_ADDRESS to enable self-awareness."
1194
+
1195
+ if lower == "history":
1196
+ hist = memory.get(sender)
1197
+ if not hist:
1198
+ return "No conversation history yet."
1199
+ lines = [f"Conversation history ({len(hist)} messages):"]
1200
+ for m in hist[-6:]:
1201
+ role = m["role"].upper()
1202
+ lines.append(f" [{role}] {m['content'][:100]}")
1203
+ return "\\n".join(lines)
1204
+
1205
+ if lower == "clear":
1206
+ memory.clear(sender)
1207
+ return "Conversation history cleared."
1208
+
1209
+ if lower in ("balance", "wallet"):
1210
+ try:
1211
+ bal = wallet.get_balance(ctx)
1212
+ return f"Wallet balance: {bal} atestfet ({bal / 1e18:.6f} FET)"
1213
+ except Exception:
1214
+ return "Balance check unavailable."
1215
+
1216
+ # --- LLM-powered response (with conversation memory) ---
1217
+
1218
+ memory.add(sender, "user", message)
1219
+
1220
+ llm_response = llm_respond(message, sender, tier)
1221
+ if llm_response:
1222
+ memory.add(sender, "assistant", llm_response)
1223
+ memory.save(ctx)
1224
+ return llm_response
1225
+
1226
+ # Fallback if no LLM configured
1227
+ return (
1228
+ f"I am {BUSINESS['name']} (role: {BUSINESS['role']}).\\n"
1229
+ f"Set ASI1_API_KEY secret to enable intelligent responses.\\n"
1230
+ f"Commands: help, services, revenue, token, balance, history, clear"
1231
+ )
1232
+
1233
+
1234
+ # ==========================================================================
1235
+ # MAIN AGENT SETUP
1236
+ # ==========================================================================
1237
+
1238
+ cache = Cache(max_size=2000)
1239
+ security = Security()
1240
+ health = Health()
1241
+ pricing = PricingTable()
1242
+ tier_mgr = TierManager(cache)
1243
+ payments = PaymentService(pricing)
1244
+ wallet = WalletManager()
1245
+ revenue = RevenueTracker()
1246
+ self_aware = SelfAwareMixin(cache)
1247
+ holdings = HoldingsManager()
1248
+
1249
+ # Usage tracking for quota enforcement
1250
+ _usage: Dict[str, List[str]] = defaultdict(list)
1251
+
1252
+ agent = Agent()
1253
+ chat_proto = Protocol(spec=chat_protocol_spec)
1254
+
1255
+ # Payment protocol (seller side)
1256
+ if PAYMENT_PROTOCOL_AVAILABLE and payment_protocol_spec is not None:
1257
+ pay_proto = Protocol(spec=payment_protocol_spec, role="seller")
1258
+ else:
1259
+ pay_proto = Protocol(name="payment", version="1.0.0")
1260
+
1261
+
1262
+ # ==========================================================================
1263
+ # CHAT PROTOCOL HANDLER
1264
+ # ==========================================================================
1265
+
1266
+
1267
+ @chat_proto.on_message(ChatMessage)
1268
+ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None:
1269
+ # 1. Acknowledge receipt
1270
+ try:
1271
+ await ctx.send(
1272
+ sender,
1273
+ ChatAcknowledgement(
1274
+ timestamp=datetime.now(), acknowledged_msg_id=msg.msg_id
1275
+ ),
1276
+ )
1277
+ except Exception as e:
1278
+ ctx.logger.error(f"Failed to send ack to {sender[:20]}: {e}")
1279
+
1280
+ # 2. Extract text
1281
+ text = " ".join(
1282
+ item.text for item in msg.content if isinstance(item, TextContent)
1283
+ ).strip()
1284
+ text = text[: BUSINESS["max_input_length"]]
1285
+
1286
+ # 3. Security check (rate limit + validation)
1287
+ clean, error = security.check(ctx, sender, text)
1288
+ if error:
1289
+ health.record(False)
1290
+ await reply(ctx, sender, error, end=True)
1291
+ return
1292
+
1293
+ Logger.audit(ctx, sender, "request")
1294
+ lower = clean.lower()
1295
+
1296
+ # 4. Built-in commands
1297
+ if lower in ("help", "?"):
1298
+ tier = tier_mgr.get_tier(sender)
1299
+ effort = self_aware.get_effort_mode()
1300
+ await reply(
1301
+ ctx,
1302
+ sender,
1303
+ f"**{BUSINESS['name']}** v{BUSINESS['version']} "
1304
+ f"(role: {BUSINESS['role']}, effort: {effort})\\n\\n"
1305
+ f"{BUSINESS['description']}\\n\\n"
1306
+ f"Your tier: {tier.upper()}\\n\\n"
1307
+ f"Commands:\\n"
1308
+ f" help — this message\\n"
1309
+ f" status — health, uptime, error rate\\n"
1310
+ f" services — available services and pricing\\n"
1311
+ f" token — token price and holder stats\\n"
1312
+ f" revenue — revenue summary (owner only)\\n"
1313
+ f" tokenize — create token (owner only)\\n"
1314
+ f" <query> — ask the agent anything",
1315
+ )
1316
+ return
1317
+
1318
+ if lower == "status":
1319
+ s = health.status()
1320
+ w = wallet.get_balance(ctx)
1321
+ effort = self_aware.get_effort_mode()
1322
+ await reply(
1323
+ ctx,
1324
+ sender,
1325
+ f"Status: {s['status']} | Uptime: {s['uptime_seconds']}s | "
1326
+ f"Requests: {s['requests']} | Error rate: {s['error_rate']} | "
1327
+ f"Balance: {w} atestfet | Effort: {effort}",
1328
+ )
1329
+ return
1330
+
1331
+ if "tokenize" in lower:
1332
+ if OWNER_ADDRESS and sender != OWNER_ADDRESS:
1333
+ await reply(ctx, sender, "Only the agent owner can trigger tokenization.", end=True)
1334
+ return
1335
+ result = AgentLaunch.tokenize()
1336
+ link = result.get("data", {}).get("handoff_link") or result.get("handoff_link")
1337
+ await reply(
1338
+ ctx,
1339
+ sender,
1340
+ f"Token created! Deploy here: {link}" if link else f"Result: {json.dumps(result)}",
1341
+ end=True,
1342
+ )
1343
+ return
1344
+
1345
+ # 5. Check tier and quota
1346
+ tier = tier_mgr.get_tier(sender)
1347
+ allowed, quota_error = tier_mgr.check_quota(sender, tier, _usage)
1348
+ if not allowed:
1349
+ health.record(False)
1350
+ await reply(ctx, sender, quota_error, end=True)
1351
+ return
1352
+
1353
+ # 6. Delegate to business logic
1354
+ try:
1355
+ response = await handle_business(ctx, sender, clean, tier)
1356
+ health.record(True)
1357
+ except Exception as e:
1358
+ health.record(False)
1359
+ Logger.error(ctx, "BUSINESS_ERROR", str(e))
1360
+ response = "Something went wrong. Please try again."
1361
+
1362
+ await reply(ctx, sender, response, end=True)
1363
+
1364
+
1365
+ @chat_proto.on_message(ChatAcknowledgement)
1366
+ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> None:
1367
+ ctx.logger.debug(f"Ack from {sender[:20]} for msg {msg.acknowledged_msg_id}")
1368
+
1369
+
1370
+ # ==========================================================================
1371
+ # PAYMENT PROTOCOL HANDLERS
1372
+ # ==========================================================================
1373
+
1374
+
1375
+ @pay_proto.on_message(CommitPayment)
1376
+ async def handle_commit(ctx: Context, sender: str, msg: CommitPayment) -> None:
1377
+ pending = await payments.on_commit(ctx, sender, msg)
1378
+ if pending:
1379
+ revenue.record_income(ctx, pending["amount"], sender, pending["service"])
1380
+ await ctx.send(
1381
+ sender,
1382
+ CompletePayment(request_id=msg.request_id, result="Payment confirmed. Service delivered."),
1383
+ )
1384
+
1385
+
1386
+ @pay_proto.on_message(RejectPayment)
1387
+ async def handle_reject(ctx: Context, sender: str, msg: RejectPayment) -> None:
1388
+ Logger.info(ctx, "PAYMENT_REJECTED", {"from": sender[:20], "reason": msg.reason})
1389
+
1390
+
1391
+ if not PAYMENT_PROTOCOL_AVAILABLE:
1392
+ @pay_proto.on_message(RequestPayment)
1393
+ async def handle_incoming_request(ctx: Context, sender: str, msg: RequestPayment) -> None:
1394
+ Logger.info(ctx, "PAYMENT_REQUEST_RECEIVED", {"from": sender[:20], "amount": msg.amount})
1395
+
1396
+
1397
+ # ==========================================================================
1398
+ # BACKGROUND TASKS
1399
+ # ==========================================================================
1400
+
1401
+
1402
+ @agent.on_interval(period=float(BUSINESS["interval_seconds"]))
1403
+ async def background_task(ctx: Context) -> None:
1404
+ """Periodic self-awareness update and health check."""
1405
+ self_aware.update(ctx)
1406
+ wallet.fund_check(ctx)
1407
+ ctx.logger.info(f"[HEALTH] {json.dumps(health.status())}")
1408
+
1409
+
1410
+ # ==========================================================================
1411
+ # WIRE UP
1412
+ # ==========================================================================
1413
+
1414
+ agent.include(chat_proto, publish_manifest=True)
1415
+ agent.include(pay_proto, publish_manifest=True)
1416
+
1417
+ if __name__ == "__main__":
1418
+ agent.run()
1419
+ `,
1420
+ };
1421
+ //# sourceMappingURL=swarm-starter.js.map