agentlaunch-templates 0.2.7 → 0.2.9

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 (39) hide show
  1. package/dist/__tests__/build.test.d.ts +12 -0
  2. package/dist/__tests__/build.test.d.ts.map +1 -0
  3. package/dist/__tests__/build.test.js +134 -0
  4. package/dist/__tests__/build.test.js.map +1 -0
  5. package/dist/__tests__/genesis-integration.test.d.ts +12 -0
  6. package/dist/__tests__/genesis-integration.test.d.ts.map +1 -0
  7. package/dist/__tests__/genesis-integration.test.js +143 -0
  8. package/dist/__tests__/genesis-integration.test.js.map +1 -0
  9. package/dist/__tests__/genesis.test.d.ts +16 -0
  10. package/dist/__tests__/genesis.test.d.ts.map +1 -0
  11. package/dist/__tests__/genesis.test.js +312 -0
  12. package/dist/__tests__/genesis.test.js.map +1 -0
  13. package/dist/claude-context.d.ts +2 -1
  14. package/dist/claude-context.d.ts.map +1 -1
  15. package/dist/claude-context.js +279 -10
  16. package/dist/claude-context.js.map +1 -1
  17. package/dist/generator.d.ts.map +1 -1
  18. package/dist/generator.js +10 -8
  19. package/dist/generator.js.map +1 -1
  20. package/dist/index.d.ts +3 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/presets.d.ts +50 -0
  25. package/dist/presets.d.ts.map +1 -0
  26. package/dist/presets.js +194 -0
  27. package/dist/presets.js.map +1 -0
  28. package/dist/registry.d.ts +15 -2
  29. package/dist/registry.d.ts.map +1 -1
  30. package/dist/registry.js +37 -3
  31. package/dist/registry.js.map +1 -1
  32. package/dist/templates/genesis.d.ts +26 -0
  33. package/dist/templates/genesis.d.ts.map +1 -0
  34. package/dist/templates/genesis.js +1197 -0
  35. package/dist/templates/genesis.js.map +1 -0
  36. package/dist/templates/gifter.d.ts.map +1 -1
  37. package/dist/templates/gifter.js +7 -3
  38. package/dist/templates/gifter.js.map +1 -1
  39. package/package.json +2 -1
@@ -0,0 +1,1197 @@
1
+ /**
2
+ * genesis.ts — Genesis Network 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: "genesis",
31
+ description: "Full commerce stack for agent swarms — payments, tiers, revenue, self-awareness, cross-holdings",
32
+ category: "Genesis Network",
33
+ variables: [
34
+ { name: "agent_name", required: true, description: "Name of the agent" },
35
+ {
36
+ name: "description",
37
+ default: "A Genesis Network 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
+ "AGENT_ADDRESS",
86
+ "AGENT_OWNER_ADDRESS",
87
+ "BSC_PRIVATE_KEY",
88
+ ],
89
+ code: `#!/usr/bin/env python3
90
+ """
91
+ {{agent_name}} — Genesis Network Agent (role: {{role}})
92
+
93
+ {{description}}
94
+
95
+ Generated by: agentlaunch scaffold {{agent_name}} --type genesis
96
+
97
+ Commerce layers (inline):
98
+ 1. Logger — structured logging with audit trail
99
+ 2. Security — rate limiting, input validation
100
+ 3. Health — uptime, error rate tracking
101
+ 4. Cache — in-memory TTL cache
102
+ 5. Revenue/Tier — PricingTable, TierManager (token-gated access)
103
+ 6. Commerce — PaymentService, WalletManager, RevenueTracker
104
+ 7. SelfAware — own token price/holder awareness
105
+ 8. Holdings — cross-token operations via web3 or handoff links
106
+ 9. SwarmBusiness — YOUR LOGIC HERE
107
+
108
+ Platform constants (source of truth: deployed smart contracts):
109
+ - Deploy fee: 120 FET (read dynamically, can change via multi-sig)
110
+ - Graduation target: 30,000 FET -> auto DEX listing
111
+ - Trading fee: 2% -> 100% to protocol treasury (NO creator fee)
112
+ """
113
+
114
+ from uagents import Agent, Context, Protocol, Model
115
+ from uagents_core.contrib.protocols.chat import (
116
+ ChatAcknowledgement,
117
+ ChatMessage,
118
+ EndSessionContent,
119
+ TextContent,
120
+ chat_protocol_spec,
121
+ )
122
+
123
+ import json
124
+ import os
125
+ import time
126
+ from collections import defaultdict
127
+ from datetime import datetime, date
128
+ from typing import Any, Dict, List, Optional, Tuple
129
+ from uuid import uuid4
130
+
131
+ import requests
132
+
133
+ # ==========================================================================
134
+ # COM-01: Payment Protocol — try official, fallback to custom models
135
+ # ==========================================================================
136
+
137
+ try:
138
+ from uagents_core.contrib.protocols.payment import (
139
+ RequestPayment,
140
+ CommitPayment,
141
+ CompletePayment,
142
+ RejectPayment,
143
+ CancelPayment,
144
+ Funds,
145
+ payment_protocol_spec,
146
+ )
147
+ PAYMENT_PROTOCOL_AVAILABLE = True
148
+ except ImportError:
149
+ PAYMENT_PROTOCOL_AVAILABLE = False
150
+
151
+ class Funds(Model):
152
+ denom: str = "atestfet"
153
+ amount: int = 0
154
+
155
+ class RequestPayment(Model):
156
+ request_id: str = ""
157
+ amount: int = 0
158
+ denom: str = "atestfet"
159
+ service: str = ""
160
+ recipient: str = ""
161
+
162
+ class CommitPayment(Model):
163
+ request_id: str = ""
164
+ tx_hash: str = ""
165
+
166
+ class CompletePayment(Model):
167
+ request_id: str = ""
168
+ result: str = ""
169
+
170
+ class RejectPayment(Model):
171
+ request_id: str = ""
172
+ reason: str = ""
173
+
174
+ class CancelPayment(Model):
175
+ request_id: str = ""
176
+ reason: str = ""
177
+
178
+ payment_protocol_spec = None
179
+
180
+
181
+ # ==========================================================================
182
+ # API CONFIG
183
+ # ==========================================================================
184
+
185
+ AGENTLAUNCH_API = os.environ.get("AGENTLAUNCH_API", "${RESOLVED_API_URL}")
186
+ OWNER_ADDRESS = os.environ.get("AGENT_OWNER_ADDRESS", "")
187
+ TOKEN_ADDRESS = os.environ.get("TOKEN_ADDRESS", "{{token_address}}")
188
+
189
+ BUSINESS = {
190
+ "name": "{{agent_name}}",
191
+ "description": "{{description}}",
192
+ "role": "{{role}}",
193
+ "version": "1.0.0",
194
+ "service_price_afet": int("{{service_price_afet}}"),
195
+ "interval_seconds": int("{{interval_seconds}}"),
196
+ "premium_token_threshold": int("{{premium_token_threshold}}"),
197
+ "effort_mode": "{{effort_mode}}",
198
+ "rate_limit_per_minute": int("{{rate_limit_per_minute}}"),
199
+ "free_requests_per_day": int("{{free_requests_per_day}}"),
200
+ "max_input_length": 5000,
201
+ }
202
+
203
+ # Bonding curve contract ABI (approve + buyTokens + sellTokens)
204
+ BONDING_CURVE_ABI = [
205
+ {"name": "buyTokens", "type": "function",
206
+ "inputs": [{"name": "tokenAddress", "type": "address"},
207
+ {"name": "minTokensOut", "type": "uint256"}],
208
+ "outputs": [], "stateMutability": "payable"},
209
+ {"name": "sellTokens", "type": "function",
210
+ "inputs": [{"name": "tokenAddress", "type": "address"},
211
+ {"name": "tokenAmount", "type": "uint256"},
212
+ {"name": "minFetOut", "type": "uint256"}],
213
+ "outputs": [], "stateMutability": "nonpayable"},
214
+ ]
215
+
216
+ ERC20_ABI = [
217
+ {"name": "approve", "type": "function",
218
+ "inputs": [{"name": "spender", "type": "address"},
219
+ {"name": "amount", "type": "uint256"}],
220
+ "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable"},
221
+ {"name": "balanceOf", "type": "function",
222
+ "inputs": [{"name": "account", "type": "address"}],
223
+ "outputs": [{"name": "", "type": "uint256"}], "stateMutability": "view"},
224
+ {"name": "decimals", "type": "function",
225
+ "inputs": [], "outputs": [{"name": "", "type": "uint8"}],
226
+ "stateMutability": "view"},
227
+ {"name": "transfer", "type": "function",
228
+ "inputs": [{"name": "to", "type": "address"},
229
+ {"name": "value", "type": "uint256"}],
230
+ "outputs": [{"name": "", "type": "bool"}], "stateMutability": "nonpayable"},
231
+ ]
232
+
233
+
234
+ # ==========================================================================
235
+ # LAYER 1: LOGGER — structured logging with audit trail
236
+ # ==========================================================================
237
+
238
+
239
+ class Logger:
240
+ @staticmethod
241
+ def info(ctx: Context, event: str, data: Optional[Dict] = None) -> None:
242
+ ctx.logger.info(f"[{event}] {json.dumps(data or {})}")
243
+
244
+ @staticmethod
245
+ def audit(ctx: Context, user: str, action: str) -> None:
246
+ ctx.logger.info(
247
+ f"[AUDIT] user={user[:20]} action={action} "
248
+ f"ts={datetime.now().isoformat()}"
249
+ )
250
+
251
+ @staticmethod
252
+ def error(ctx: Context, event: str, error: str) -> None:
253
+ ctx.logger.error(f"[{event}] {error}")
254
+
255
+
256
+ # ==========================================================================
257
+ # LAYER 2: SECURITY — rate limiting, input validation
258
+ # ==========================================================================
259
+
260
+
261
+ class Security:
262
+ def __init__(self) -> None:
263
+ self._requests: Dict[str, List[float]] = defaultdict(list)
264
+ self._check_count: int = 0
265
+
266
+ def check(self, ctx: Context, user_id: str, message: str) -> Tuple[Optional[str], Optional[str]]:
267
+ now = time.time()
268
+
269
+ # Sliding window rate limit
270
+ self._requests[user_id] = [
271
+ t for t in self._requests[user_id] if now - t < 60
272
+ ]
273
+ if len(self._requests[user_id]) >= BUSINESS["rate_limit_per_minute"]:
274
+ return None, "Rate limit exceeded. Please wait a moment."
275
+ self._requests[user_id].append(now)
276
+
277
+ # Periodic cleanup
278
+ self._check_count += 1
279
+ if self._check_count % 100 == 0:
280
+ stale = [
281
+ k for k, v in self._requests.items()
282
+ if not v or (now - max(v)) > 300
283
+ ]
284
+ for k in stale:
285
+ del self._requests[k]
286
+
287
+ # Input validation
288
+ if not message or not message.strip():
289
+ return None, "Empty message."
290
+ if len(message) > BUSINESS["max_input_length"]:
291
+ return None, f"Message too long (max {BUSINESS['max_input_length']} chars)."
292
+
293
+ return message.strip(), None
294
+
295
+
296
+ # ==========================================================================
297
+ # LAYER 3: HEALTH — uptime, error rate tracking
298
+ # ==========================================================================
299
+
300
+
301
+ class Health:
302
+ def __init__(self) -> None:
303
+ self._start: datetime = datetime.now()
304
+ self._requests: int = 0
305
+ self._errors: int = 0
306
+
307
+ def record(self, success: bool) -> None:
308
+ self._requests += 1
309
+ if not success:
310
+ self._errors += 1
311
+
312
+ def status(self) -> Dict[str, Any]:
313
+ uptime = (datetime.now() - self._start).total_seconds()
314
+ error_rate = (self._errors / self._requests * 100) if self._requests else 0
315
+ return {
316
+ "status": "healthy" if error_rate < 10 else "degraded",
317
+ "uptime_seconds": int(uptime),
318
+ "requests": self._requests,
319
+ "error_rate": f"{error_rate:.1f}%",
320
+ }
321
+
322
+
323
+ # ==========================================================================
324
+ # LAYER 4: CACHE — in-memory TTL cache
325
+ # ==========================================================================
326
+
327
+
328
+ class Cache:
329
+ def __init__(self, max_size: int = 1000) -> None:
330
+ self._data: Dict[str, tuple] = {}
331
+ self._max_size: int = max_size
332
+
333
+ def get(self, key: str) -> Any:
334
+ if key in self._data:
335
+ value, expires = self._data[key]
336
+ if expires > time.time():
337
+ return value
338
+ del self._data[key]
339
+ return None
340
+
341
+ def set(self, key: str, value: Any, ttl: int = 300) -> None:
342
+ if len(self._data) >= self._max_size:
343
+ now = time.time()
344
+ expired = [k for k, (_, exp) in self._data.items() if exp <= now]
345
+ for k in expired:
346
+ del self._data[k]
347
+ if len(self._data) >= self._max_size:
348
+ to_drop = sorted(self._data.items(), key=lambda x: x[1][1])[
349
+ : self._max_size // 10
350
+ ]
351
+ for k, _ in to_drop:
352
+ del self._data[k]
353
+ self._data[key] = (value, time.time() + ttl)
354
+
355
+
356
+ # ==========================================================================
357
+ # LAYER 5: REVENUE/TIER — PricingTable + TierManager (COM-03)
358
+ # ==========================================================================
359
+
360
+
361
+ class PricingTable:
362
+ """Per-service pricing stored in ctx.storage."""
363
+
364
+ STORAGE_KEY = "pricing_table"
365
+
366
+ def get_price(self, ctx: Context, service: str) -> int:
367
+ table = json.loads(ctx.storage.get(self.STORAGE_KEY) or "{}")
368
+ return table.get(service, BUSINESS["service_price_afet"])
369
+
370
+ def set_price(self, ctx: Context, service: str, amount_afet: int) -> None:
371
+ table = json.loads(ctx.storage.get(self.STORAGE_KEY) or "{}")
372
+ table[service] = amount_afet
373
+ ctx.storage.set(self.STORAGE_KEY, json.dumps(table))
374
+
375
+ def list_services(self, ctx: Context) -> Dict[str, int]:
376
+ table = json.loads(ctx.storage.get(self.STORAGE_KEY) or "{}")
377
+ if not table:
378
+ table["default"] = BUSINESS["service_price_afet"]
379
+ return table
380
+
381
+
382
+ class TierManager:
383
+ """Checks token holdings via AgentLaunch API. Returns free or premium."""
384
+
385
+ def __init__(self, cache: Cache) -> None:
386
+ self._cache = cache
387
+
388
+ def get_tier(self, sender: str) -> str:
389
+ if not TOKEN_ADDRESS:
390
+ return "free"
391
+ cached = self._cache.get(f"tier:{sender}")
392
+ if cached is not None:
393
+ return cached
394
+ try:
395
+ r = requests.get(
396
+ f"{AGENTLAUNCH_API}/agents/token/{TOKEN_ADDRESS}/holders",
397
+ timeout=5,
398
+ )
399
+ if r.status_code == 200:
400
+ holders = r.json() if isinstance(r.json(), list) else r.json().get("holders", [])
401
+ for h in holders:
402
+ addr = h.get("address", h.get("holder", ""))
403
+ if addr.lower() == sender.lower():
404
+ balance = int(h.get("balance", h.get("amount", 0)))
405
+ tier = "premium" if balance >= BUSINESS["premium_token_threshold"] else "free"
406
+ self._cache.set(f"tier:{sender}", tier, ttl=300)
407
+ return tier
408
+ except Exception:
409
+ pass
410
+ self._cache.set(f"tier:{sender}", "free", ttl=300)
411
+ return "free"
412
+
413
+ def check_quota(self, user_id: str, tier: str, usage: Dict) -> Tuple[bool, Optional[str]]:
414
+ today = date.today().isoformat()
415
+ user_usage = usage.get(user_id, [])
416
+ user_usage = [t for t in user_usage if t.startswith(today)]
417
+ usage[user_id] = user_usage
418
+ today_count = len(user_usage)
419
+ limit = 10000 if tier == "premium" else BUSINESS["free_requests_per_day"]
420
+ if today_count >= limit:
421
+ if tier == "free":
422
+ return False, (
423
+ f"Free limit reached ({limit}/day). "
424
+ f"Hold {BUSINESS['premium_token_threshold']} tokens for premium!"
425
+ )
426
+ return False, f"Daily limit reached ({limit}/day)."
427
+ user_usage.append(datetime.now().isoformat())
428
+ usage[user_id] = user_usage
429
+ return True, None
430
+
431
+
432
+ # ==========================================================================
433
+ # LAYER 6: COMMERCE — PaymentService, WalletManager, RevenueTracker (COM-02/04)
434
+ # ==========================================================================
435
+
436
+
437
+ class PaymentService:
438
+ """Seller-side payment handling. Tracks pending requests and transaction log."""
439
+
440
+ def __init__(self, pricing: PricingTable) -> None:
441
+ self._pricing = pricing
442
+ self._pending: Dict[str, dict] = {}
443
+
444
+ async def charge(
445
+ self, ctx: Context, sender: str, amount_afet: int, service: str
446
+ ) -> str:
447
+ request_id = str(uuid4())
448
+ self._pending[request_id] = {
449
+ "sender": sender,
450
+ "amount": amount_afet,
451
+ "service": service,
452
+ "ts": datetime.now().isoformat(),
453
+ }
454
+ msg = RequestPayment(
455
+ request_id=request_id,
456
+ amount=amount_afet,
457
+ denom="atestfet",
458
+ service=service,
459
+ recipient=str(ctx.wallet.address()) if hasattr(ctx, "wallet") else "",
460
+ )
461
+ await ctx.send(sender, msg)
462
+ Logger.info(ctx, "CHARGE_SENT", {"to": sender[:20], "amount": amount_afet, "service": service})
463
+ return request_id
464
+
465
+ async def on_commit(self, ctx: Context, sender: str, msg) -> Optional[dict]:
466
+ request_id = getattr(msg, "request_id", "")
467
+ if request_id not in self._pending:
468
+ Logger.error(ctx, "UNKNOWN_PAYMENT", f"request_id={request_id}")
469
+ return None
470
+ pending = self._pending.pop(request_id)
471
+ # Log the transaction
472
+ tx_log = json.loads(ctx.storage.get("tx_log") or "[]")
473
+ tx_log.append({
474
+ "type": "income",
475
+ "request_id": request_id,
476
+ "sender": sender[:20],
477
+ "amount": pending["amount"],
478
+ "service": pending["service"],
479
+ "tx_hash": getattr(msg, "tx_hash", ""),
480
+ "ts": datetime.now().isoformat(),
481
+ })
482
+ ctx.storage.set("tx_log", json.dumps(tx_log[-1000:]))
483
+ Logger.info(ctx, "PAYMENT_RECEIVED", {"from": sender[:20], "amount": pending["amount"]})
484
+ return pending
485
+
486
+ def get_balance(self, ctx: Context) -> int:
487
+ try:
488
+ if hasattr(ctx, "ledger") and hasattr(ctx, "wallet"):
489
+ balance = ctx.ledger.query_bank_balance(
490
+ str(ctx.wallet.address()), "atestfet"
491
+ )
492
+ return int(balance)
493
+ except Exception:
494
+ pass
495
+ return 0
496
+
497
+
498
+ class WalletManager:
499
+ """Balance queries and fund alerts."""
500
+
501
+ def get_balance(self, ctx: Context) -> int:
502
+ try:
503
+ if hasattr(ctx, "ledger") and hasattr(ctx, "wallet"):
504
+ return int(ctx.ledger.query_bank_balance(
505
+ str(ctx.wallet.address()), "atestfet"
506
+ ))
507
+ except Exception:
508
+ pass
509
+ return 0
510
+
511
+ def get_address(self, ctx: Context) -> str:
512
+ try:
513
+ if hasattr(ctx, "wallet"):
514
+ return str(ctx.wallet.address())
515
+ except Exception:
516
+ pass
517
+ return ""
518
+
519
+ def fund_check(self, ctx: Context, min_balance: int = 10_000_000_000_000_000) -> bool:
520
+ balance = self.get_balance(ctx)
521
+ if balance < min_balance:
522
+ Logger.info(ctx, "LOW_FUNDS", {
523
+ "balance": balance,
524
+ "min_required": min_balance,
525
+ "deficit": min_balance - balance,
526
+ })
527
+ return False
528
+ return True
529
+
530
+
531
+ class RevenueTracker:
532
+ """Income/expense log in ctx.storage."""
533
+
534
+ STORAGE_KEY = "revenue_log"
535
+
536
+ def record_income(
537
+ self, ctx: Context, amount: int, source: str, service: str
538
+ ) -> None:
539
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
540
+ log.append({
541
+ "type": "income",
542
+ "amount": amount,
543
+ "source": source[:20],
544
+ "service": service,
545
+ "ts": datetime.now().isoformat(),
546
+ })
547
+ ctx.storage.set(self.STORAGE_KEY, json.dumps(log[-5000:]))
548
+
549
+ def record_expense(
550
+ self, ctx: Context, amount: int, dest: str, service: str
551
+ ) -> None:
552
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
553
+ log.append({
554
+ "type": "expense",
555
+ "amount": amount,
556
+ "dest": dest[:20],
557
+ "service": service,
558
+ "ts": datetime.now().isoformat(),
559
+ })
560
+ ctx.storage.set(self.STORAGE_KEY, json.dumps(log[-5000:]))
561
+
562
+ def get_summary(self, ctx: Context) -> Dict[str, Any]:
563
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
564
+ total_income = sum(e["amount"] for e in log if e["type"] == "income")
565
+ total_expense = sum(e["amount"] for e in log if e["type"] == "expense")
566
+ return {
567
+ "total_income_afet": total_income,
568
+ "total_expense_afet": total_expense,
569
+ "net_afet": total_income - total_expense,
570
+ "tx_count": len(log),
571
+ }
572
+
573
+ def get_daily_summary(self, ctx: Context) -> Dict[str, Any]:
574
+ log = json.loads(ctx.storage.get(self.STORAGE_KEY) or "[]")
575
+ today = date.today().isoformat()
576
+ today_entries = [e for e in log if e["ts"].startswith(today)]
577
+ income = sum(e["amount"] for e in today_entries if e["type"] == "income")
578
+ expense = sum(e["amount"] for e in today_entries if e["type"] == "expense")
579
+ return {
580
+ "date": today,
581
+ "income_afet": income,
582
+ "expense_afet": expense,
583
+ "net_afet": income - expense,
584
+ "tx_count": len(today_entries),
585
+ }
586
+
587
+
588
+ # ==========================================================================
589
+ # LAYER 7: SELF-AWARE — own token price/holder awareness (COM-05)
590
+ # ==========================================================================
591
+
592
+
593
+ class SelfAwareMixin:
594
+ """Reads own token price + holders from AgentLaunch API."""
595
+
596
+ HISTORY_KEY = "price_history"
597
+
598
+ def __init__(self, cache: Cache) -> None:
599
+ self._cache = cache
600
+ self._price: float = 0.0
601
+ self._holders: int = 0
602
+ self._market_cap: float = 0.0
603
+ self._ma_7d: float = 0.0
604
+ self._effort_mode: str = BUSINESS["effort_mode"]
605
+
606
+ def update(self, ctx: Context) -> None:
607
+ if not TOKEN_ADDRESS:
608
+ return
609
+ cached = self._cache.get("self_aware_data")
610
+ if cached:
611
+ self._price, self._holders, self._market_cap = cached
612
+ self._update_effort()
613
+ return
614
+ try:
615
+ r = requests.get(
616
+ f"{AGENTLAUNCH_API}/tokens/address/{TOKEN_ADDRESS}",
617
+ timeout=5,
618
+ )
619
+ if r.status_code == 200:
620
+ data = r.json()
621
+ token = data if "price" in data else data.get("token", data.get("data", {}))
622
+ self._price = float(token.get("price", token.get("currentPrice", 0)))
623
+ self._holders = int(token.get("holders", token.get("holderCount", 0)))
624
+ self._market_cap = float(token.get("marketCap", token.get("market_cap", 0)))
625
+ self._cache.set("self_aware_data", (self._price, self._holders, self._market_cap), ttl=60)
626
+
627
+ # Store price history (up to 30 days of hourly samples)
628
+ history = json.loads(ctx.storage.get(self.HISTORY_KEY) or "[]")
629
+ history.append({
630
+ "price": self._price,
631
+ "holders": self._holders,
632
+ "ts": datetime.now().isoformat(),
633
+ })
634
+ history = history[-720:] # 30 days * 24 hours
635
+ ctx.storage.set(self.HISTORY_KEY, json.dumps(history))
636
+
637
+ # Calculate 7-day moving average
638
+ recent = history[-168:] # 7 days * 24 hours
639
+ if recent:
640
+ self._ma_7d = sum(p["price"] for p in recent) / len(recent)
641
+
642
+ self._update_effort()
643
+ Logger.info(ctx, "SELF_AWARE_UPDATE", {
644
+ "price": self._price,
645
+ "holders": self._holders,
646
+ "ma_7d": round(self._ma_7d, 8),
647
+ "effort": self._effort_mode,
648
+ })
649
+ except Exception as e:
650
+ Logger.error(ctx, "SELF_AWARE_FETCH", str(e))
651
+
652
+ def _update_effort(self) -> None:
653
+ if self._ma_7d <= 0:
654
+ self._effort_mode = BUSINESS["effort_mode"]
655
+ return
656
+ ratio = self._price / self._ma_7d if self._ma_7d > 0 else 1.0
657
+ if ratio > 1.1:
658
+ self._effort_mode = "boost"
659
+ elif ratio < 0.9:
660
+ self._effort_mode = "conserve"
661
+ else:
662
+ self._effort_mode = "normal"
663
+
664
+ def get_effort_mode(self) -> str:
665
+ return self._effort_mode
666
+
667
+ def get_token_summary(self) -> Dict[str, Any]:
668
+ return {
669
+ "token_address": TOKEN_ADDRESS,
670
+ "price": self._price,
671
+ "holders": self._holders,
672
+ "market_cap": self._market_cap,
673
+ "ma_7d": round(self._ma_7d, 8),
674
+ "effort_mode": self._effort_mode,
675
+ }
676
+
677
+
678
+ # ==========================================================================
679
+ # LAYER 8: CROSS-HOLDINGS — HoldingsManager (COM-06)
680
+ # ==========================================================================
681
+
682
+
683
+ class HoldingsManager:
684
+ """
685
+ Direct on-chain token operations using web3 + eth_account.
686
+ Fallback: generate handoff links for human signing.
687
+ """
688
+
689
+ BSC_RPC = os.environ.get("BSC_RPC", "https://data-seed-prebsc-1-s1.binance.org:8545")
690
+ FRONTEND_URL = os.environ.get("AGENTLAUNCH_FRONTEND", "https://agent-launch.ai")
691
+
692
+ def buy_via_web3(
693
+ self, ctx: Context, token_address: str, fet_amount_wei: int,
694
+ bonding_curve_address: str = "",
695
+ ) -> Tuple[bool, str]:
696
+ try:
697
+ from web3 import Web3
698
+
699
+ private_key = os.environ.get("BSC_PRIVATE_KEY", "")
700
+ if not private_key:
701
+ return False, self.generate_buy_link(token_address, fet_amount_wei)
702
+
703
+ w3 = Web3(Web3.HTTPProvider(self.BSC_RPC))
704
+ if not w3.is_connected():
705
+ return False, "Cannot connect to BSC RPC."
706
+
707
+ account = w3.eth.account.from_key(private_key)
708
+ nonce = w3.eth.get_transaction_count(account.address)
709
+
710
+ if not bonding_curve_address:
711
+ return False, self.generate_buy_link(token_address, fet_amount_wei)
712
+
713
+ # Approve FET spend on bonding curve
714
+ # Then call buyTokens
715
+ curve = w3.eth.contract(
716
+ address=Web3.to_checksum_address(bonding_curve_address),
717
+ abi=BONDING_CURVE_ABI,
718
+ )
719
+ tx = curve.functions.buyTokens(
720
+ Web3.to_checksum_address(token_address), 0
721
+ ).build_transaction({
722
+ "from": account.address,
723
+ "value": fet_amount_wei,
724
+ "nonce": nonce,
725
+ "gas": 300000,
726
+ "gasPrice": w3.eth.gas_price,
727
+ "chainId": w3.eth.chain_id,
728
+ })
729
+ signed = w3.eth.account.sign_transaction(tx, private_key)
730
+ tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
731
+ Logger.info(ctx, "BUY_VIA_WEB3", {
732
+ "token": token_address[:12],
733
+ "amount": fet_amount_wei,
734
+ "tx": tx_hash.hex(),
735
+ })
736
+ return True, f"0x{tx_hash.hex()}"
737
+
738
+ except ImportError:
739
+ return False, self.generate_buy_link(token_address, fet_amount_wei)
740
+ except Exception as e:
741
+ return False, str(e)
742
+
743
+ def sell_via_web3(
744
+ self, ctx: Context, token_address: str, token_amount: int,
745
+ bonding_curve_address: str = "",
746
+ ) -> Tuple[bool, str]:
747
+ try:
748
+ from web3 import Web3
749
+
750
+ private_key = os.environ.get("BSC_PRIVATE_KEY", "")
751
+ if not private_key:
752
+ return False, self.generate_sell_link(token_address, token_amount)
753
+
754
+ w3 = Web3(Web3.HTTPProvider(self.BSC_RPC))
755
+ if not w3.is_connected():
756
+ return False, "Cannot connect to BSC RPC."
757
+
758
+ account = w3.eth.account.from_key(private_key)
759
+ nonce = w3.eth.get_transaction_count(account.address)
760
+
761
+ if not bonding_curve_address:
762
+ return False, self.generate_sell_link(token_address, token_amount)
763
+
764
+ # Approve token spend on bonding curve, then sell
765
+ token_contract = w3.eth.contract(
766
+ address=Web3.to_checksum_address(token_address),
767
+ abi=ERC20_ABI,
768
+ )
769
+ approve_tx = token_contract.functions.approve(
770
+ Web3.to_checksum_address(bonding_curve_address), token_amount
771
+ ).build_transaction({
772
+ "from": account.address,
773
+ "nonce": nonce,
774
+ "gas": 100000,
775
+ "gasPrice": w3.eth.gas_price,
776
+ "chainId": w3.eth.chain_id,
777
+ })
778
+ signed_approve = w3.eth.account.sign_transaction(approve_tx, private_key)
779
+ w3.eth.send_raw_transaction(signed_approve.raw_transaction)
780
+
781
+ nonce += 1
782
+ curve = w3.eth.contract(
783
+ address=Web3.to_checksum_address(bonding_curve_address),
784
+ abi=BONDING_CURVE_ABI,
785
+ )
786
+ sell_tx = curve.functions.sellTokens(
787
+ Web3.to_checksum_address(token_address), token_amount, 0
788
+ ).build_transaction({
789
+ "from": account.address,
790
+ "nonce": nonce,
791
+ "gas": 300000,
792
+ "gasPrice": w3.eth.gas_price,
793
+ "chainId": w3.eth.chain_id,
794
+ })
795
+ signed_sell = w3.eth.account.sign_transaction(sell_tx, private_key)
796
+ tx_hash = w3.eth.send_raw_transaction(signed_sell.raw_transaction)
797
+ Logger.info(ctx, "SELL_VIA_WEB3", {
798
+ "token": token_address[:12],
799
+ "amount": token_amount,
800
+ "tx": tx_hash.hex(),
801
+ })
802
+ return True, f"0x{tx_hash.hex()}"
803
+
804
+ except ImportError:
805
+ return False, self.generate_sell_link(token_address, token_amount)
806
+ except Exception as e:
807
+ return False, str(e)
808
+
809
+ def get_holdings_summary(self, ctx: Context, token_addresses: List[str]) -> List[Dict]:
810
+ results = []
811
+ for addr in token_addresses:
812
+ try:
813
+ r = requests.get(
814
+ f"{AGENTLAUNCH_API}/tokens/address/{addr}",
815
+ timeout=5,
816
+ )
817
+ if r.status_code == 200:
818
+ data = r.json()
819
+ token = data if "name" in data else data.get("token", data.get("data", {}))
820
+ results.append({
821
+ "address": addr,
822
+ "name": token.get("name", "Unknown"),
823
+ "price": float(token.get("price", token.get("currentPrice", 0))),
824
+ "holders": int(token.get("holders", token.get("holderCount", 0))),
825
+ })
826
+ except Exception:
827
+ results.append({"address": addr, "name": "Error", "price": 0, "holders": 0})
828
+ return results
829
+
830
+ @staticmethod
831
+ def generate_buy_link(token_address: str, amount: int = 0) -> str:
832
+ base = os.environ.get("AGENTLAUNCH_FRONTEND", "https://agent-launch.ai")
833
+ url = f"{base}/trade/{token_address}?action=buy"
834
+ if amount:
835
+ url += f"&amount={amount}"
836
+ return f"Sign here: {url}"
837
+
838
+ @staticmethod
839
+ def generate_sell_link(token_address: str, amount: int = 0) -> str:
840
+ base = os.environ.get("AGENTLAUNCH_FRONTEND", "https://agent-launch.ai")
841
+ url = f"{base}/trade/{token_address}?action=sell"
842
+ if amount:
843
+ url += f"&amount={amount}"
844
+ return f"Sign here: {url}"
845
+
846
+
847
+ # ==========================================================================
848
+ # AGENTLAUNCH INTEGRATION — tokenization via API
849
+ # ==========================================================================
850
+
851
+
852
+ class AgentLaunch:
853
+ @staticmethod
854
+ def tokenize() -> Dict:
855
+ agent_address = os.environ.get("AGENT_ADDRESS")
856
+ if not agent_address:
857
+ return {"error": "AGENT_ADDRESS env var not set."}
858
+ try:
859
+ r = requests.post(
860
+ f"{AGENTLAUNCH_API}/agents/tokenize",
861
+ headers={
862
+ "X-API-Key": os.environ.get("AGENTLAUNCH_API_KEY", ""),
863
+ "Content-Type": "application/json",
864
+ },
865
+ json={
866
+ "agentAddress": agent_address,
867
+ "name": BUSINESS["name"],
868
+ "description": BUSINESS["description"],
869
+ },
870
+ timeout=30,
871
+ )
872
+ return r.json() if r.status_code in [200, 201] else {"error": r.text}
873
+ except Exception as e:
874
+ return {"error": str(e)}
875
+
876
+
877
+ # ==========================================================================
878
+ # REPLY HELPER
879
+ # ==========================================================================
880
+
881
+
882
+ async def reply(ctx: Context, sender: str, text: str, end: bool = False) -> None:
883
+ content = [TextContent(type="text", text=text)]
884
+ if end:
885
+ content.append(EndSessionContent(type="end-session"))
886
+ try:
887
+ await ctx.send(
888
+ sender,
889
+ ChatMessage(timestamp=datetime.now(), msg_id=uuid4(), content=content),
890
+ )
891
+ except Exception as e:
892
+ ctx.logger.error(f"Failed to send reply to {sender[:20]}: {e}")
893
+
894
+
895
+ # ==========================================================================
896
+ # LAYER 9: SWARM BUSINESS — YOUR LOGIC HERE
897
+ # ==========================================================================
898
+ #
899
+ # Everything above is the Genesis commerce stack. Below is where you add
900
+ # your agent's unique intelligence. The commerce layers handle payments,
901
+ # tiers, and revenue. Focus on what your agent DOES, not how it gets paid.
902
+ #
903
+ # The default handler below routes standard commands (help, status, tokenize)
904
+ # and delegates business queries to the handle_business() function.
905
+ #
906
+ # Examples of what to add:
907
+ #
908
+ # async def handle_business(ctx, sender, message, tier):
909
+ # # Your agent's core logic goes here
910
+ # return "result"
911
+ #
912
+ # @agent.on_interval(period=BUSINESS["interval_seconds"])
913
+ # async def background_task(ctx):
914
+ # # Periodic work: monitoring, data collection, alerts
915
+ # pass
916
+
917
+
918
+ async def handle_business(
919
+ ctx: Context, sender: str, message: str, tier: str
920
+ ) -> str:
921
+ """
922
+ ===================================================================
923
+ YOUR SWARM LOGIC — This is where your agent becomes unique
924
+ ===================================================================
925
+
926
+ Add your message handlers, interval tasks, and service logic here.
927
+ The commerce layers above handle payments, tiers, and revenue.
928
+ Focus on what your agent DOES, not how it gets paid.
929
+
930
+ Available objects:
931
+ pricing — PricingTable: get_price(), set_price(), list_services()
932
+ tier_mgr — TierManager: get_tier(), check_quota()
933
+ payments — PaymentService: charge(), on_commit(), get_balance()
934
+ wallet — WalletManager: get_balance(), get_address(), fund_check()
935
+ revenue — RevenueTracker: record_income/expense(), get_summary()
936
+ self_aware — SelfAwareMixin: update(), get_effort_mode(), get_token_summary()
937
+ holdings — HoldingsManager: buy_via_web3(), sell_via_web3(), get_holdings_summary()
938
+ cache — Cache: get(), set()
939
+
940
+ Args:
941
+ ctx: Agent context (has logger, storage, ledger, wallet)
942
+ sender: Address of the message sender
943
+ message: The cleaned user message
944
+ tier: "free" or "premium"
945
+
946
+ Returns:
947
+ Response text to send back to the user
948
+ """
949
+ lower = message.lower()
950
+
951
+ # Example: price query
952
+ if lower.startswith("price "):
953
+ parts = message.split(maxsplit=1)
954
+ if len(parts) > 1:
955
+ services = pricing.list_services(ctx)
956
+ svc = parts[1].strip()
957
+ if svc in services:
958
+ price = services[svc]
959
+ return f"Service '{svc}' costs {price} atestfet ({price / 1e18:.6f} FET)"
960
+ return f"Unknown service '{svc}'. Available: {', '.join(services.keys())}"
961
+ return "Usage: price <service_name>"
962
+
963
+ # Example: list available services
964
+ if lower in ("services", "pricing", "menu"):
965
+ services = pricing.list_services(ctx)
966
+ lines = [f"Available services for {BUSINESS['name']}:"]
967
+ for svc, price in services.items():
968
+ lines.append(f" {svc}: {price} atestfet ({price / 1e18:.6f} FET)")
969
+ lines.append(f"\\nYour tier: {tier.upper()}")
970
+ return "\\n".join(lines)
971
+
972
+ # Example: revenue summary (owner only)
973
+ if lower == "revenue" and (not OWNER_ADDRESS or sender == OWNER_ADDRESS):
974
+ summary = revenue.get_summary(ctx)
975
+ daily = revenue.get_daily_summary(ctx)
976
+ return (
977
+ f"Revenue Summary:\\n"
978
+ f" All-time income: {summary['total_income_afet']} atestfet\\n"
979
+ f" All-time expense: {summary['total_expense_afet']} atestfet\\n"
980
+ f" Net: {summary['net_afet']} atestfet\\n"
981
+ f" Transactions: {summary['tx_count']}\\n\\n"
982
+ f"Today ({daily['date']}):\\n"
983
+ f" Income: {daily['income_afet']} atestfet\\n"
984
+ f" Expense: {daily['expense_afet']} atestfet"
985
+ )
986
+
987
+ # Example: token self-awareness
988
+ if lower in ("token", "self", "awareness"):
989
+ if TOKEN_ADDRESS:
990
+ s = self_aware.get_token_summary()
991
+ return (
992
+ f"Token Status:\\n"
993
+ f" Address: {s['token_address'][:16]}...\\n"
994
+ f" Price: {s['price']:.8f} FET\\n"
995
+ f" Holders: {s['holders']}\\n"
996
+ f" 7d MA: {s['ma_7d']:.8f} FET\\n"
997
+ f" Market Cap: {s.get('market_cap', 0):.2f} FET\\n"
998
+ f" Effort Mode: {s['effort_mode'].upper()}"
999
+ )
1000
+ return "No token address configured. Set TOKEN_ADDRESS to enable self-awareness."
1001
+
1002
+ # Default: echo with role context
1003
+ return (
1004
+ f"I am {BUSINESS['name']} (role: {BUSINESS['role']}).\\n"
1005
+ f"Type 'help' for commands, 'services' for pricing, or ask me anything.\\n\\n"
1006
+ f"Your message: {message[:200]}"
1007
+ )
1008
+
1009
+
1010
+ # ==========================================================================
1011
+ # MAIN AGENT SETUP
1012
+ # ==========================================================================
1013
+
1014
+ cache = Cache(max_size=2000)
1015
+ security = Security()
1016
+ health = Health()
1017
+ pricing = PricingTable()
1018
+ tier_mgr = TierManager(cache)
1019
+ payments = PaymentService(pricing)
1020
+ wallet = WalletManager()
1021
+ revenue = RevenueTracker()
1022
+ self_aware = SelfAwareMixin(cache)
1023
+ holdings = HoldingsManager()
1024
+
1025
+ # Usage tracking for quota enforcement
1026
+ _usage: Dict[str, List[str]] = defaultdict(list)
1027
+
1028
+ agent = Agent()
1029
+ chat_proto = Protocol(spec=chat_protocol_spec)
1030
+
1031
+ # Payment protocol (seller side)
1032
+ if PAYMENT_PROTOCOL_AVAILABLE and payment_protocol_spec is not None:
1033
+ pay_proto = agent.create_protocol(spec=payment_protocol_spec, role="seller")
1034
+ else:
1035
+ pay_proto = Protocol(name="payment", version="1.0.0")
1036
+
1037
+
1038
+ # ==========================================================================
1039
+ # CHAT PROTOCOL HANDLER
1040
+ # ==========================================================================
1041
+
1042
+
1043
+ @chat_proto.on_message(ChatMessage)
1044
+ async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None:
1045
+ # 1. Acknowledge receipt
1046
+ try:
1047
+ await ctx.send(
1048
+ sender,
1049
+ ChatAcknowledgement(
1050
+ timestamp=datetime.now(), acknowledged_msg_id=msg.msg_id
1051
+ ),
1052
+ )
1053
+ except Exception as e:
1054
+ ctx.logger.error(f"Failed to send ack to {sender[:20]}: {e}")
1055
+
1056
+ # 2. Extract text
1057
+ text = " ".join(
1058
+ item.text for item in msg.content if isinstance(item, TextContent)
1059
+ ).strip()
1060
+ text = text[: BUSINESS["max_input_length"]]
1061
+
1062
+ # 3. Security check (rate limit + validation)
1063
+ clean, error = security.check(ctx, sender, text)
1064
+ if error:
1065
+ health.record(False)
1066
+ await reply(ctx, sender, error, end=True)
1067
+ return
1068
+
1069
+ Logger.audit(ctx, sender, "request")
1070
+ lower = clean.lower()
1071
+
1072
+ # 4. Built-in commands
1073
+ if lower in ("help", "?"):
1074
+ tier = tier_mgr.get_tier(sender)
1075
+ effort = self_aware.get_effort_mode()
1076
+ await reply(
1077
+ ctx,
1078
+ sender,
1079
+ f"**{BUSINESS['name']}** v{BUSINESS['version']} "
1080
+ f"(role: {BUSINESS['role']}, effort: {effort})\\n\\n"
1081
+ f"{BUSINESS['description']}\\n\\n"
1082
+ f"Your tier: {tier.upper()}\\n\\n"
1083
+ f"Commands:\\n"
1084
+ f" help — this message\\n"
1085
+ f" status — health, uptime, error rate\\n"
1086
+ f" services — available services and pricing\\n"
1087
+ f" token — token price and holder stats\\n"
1088
+ f" revenue — revenue summary (owner only)\\n"
1089
+ f" tokenize — create token (owner only)\\n"
1090
+ f" <query> — ask the agent anything",
1091
+ )
1092
+ return
1093
+
1094
+ if lower == "status":
1095
+ s = health.status()
1096
+ w = wallet.get_balance(ctx)
1097
+ effort = self_aware.get_effort_mode()
1098
+ await reply(
1099
+ ctx,
1100
+ sender,
1101
+ f"Status: {s['status']} | Uptime: {s['uptime_seconds']}s | "
1102
+ f"Requests: {s['requests']} | Error rate: {s['error_rate']} | "
1103
+ f"Balance: {w} atestfet | Effort: {effort}",
1104
+ )
1105
+ return
1106
+
1107
+ if "tokenize" in lower:
1108
+ if OWNER_ADDRESS and sender != OWNER_ADDRESS:
1109
+ await reply(ctx, sender, "Only the agent owner can trigger tokenization.", end=True)
1110
+ return
1111
+ result = AgentLaunch.tokenize()
1112
+ link = result.get("data", {}).get("handoff_link") or result.get("handoff_link")
1113
+ await reply(
1114
+ ctx,
1115
+ sender,
1116
+ f"Token created! Deploy here: {link}" if link else f"Result: {json.dumps(result)}",
1117
+ end=True,
1118
+ )
1119
+ return
1120
+
1121
+ # 5. Check tier and quota
1122
+ tier = tier_mgr.get_tier(sender)
1123
+ allowed, quota_error = tier_mgr.check_quota(sender, tier, _usage)
1124
+ if not allowed:
1125
+ health.record(False)
1126
+ await reply(ctx, sender, quota_error, end=True)
1127
+ return
1128
+
1129
+ # 6. Delegate to business logic
1130
+ try:
1131
+ response = await handle_business(ctx, sender, clean, tier)
1132
+ health.record(True)
1133
+ except Exception as e:
1134
+ health.record(False)
1135
+ Logger.error(ctx, "BUSINESS_ERROR", str(e))
1136
+ response = "Something went wrong. Please try again."
1137
+
1138
+ await reply(ctx, sender, response, end=True)
1139
+
1140
+
1141
+ @chat_proto.on_message(ChatAcknowledgement)
1142
+ async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> None:
1143
+ ctx.logger.debug(f"Ack from {sender[:20]} for msg {msg.acknowledged_msg_id}")
1144
+
1145
+
1146
+ # ==========================================================================
1147
+ # PAYMENT PROTOCOL HANDLERS
1148
+ # ==========================================================================
1149
+
1150
+
1151
+ @pay_proto.on_message(CommitPayment)
1152
+ async def handle_commit(ctx: Context, sender: str, msg: CommitPayment) -> None:
1153
+ pending = await payments.on_commit(ctx, sender, msg)
1154
+ if pending:
1155
+ revenue.record_income(ctx, pending["amount"], sender, pending["service"])
1156
+ await ctx.send(
1157
+ sender,
1158
+ CompletePayment(request_id=msg.request_id, result="Payment confirmed. Service delivered."),
1159
+ )
1160
+
1161
+
1162
+ @pay_proto.on_message(RejectPayment)
1163
+ async def handle_reject(ctx: Context, sender: str, msg: RejectPayment) -> None:
1164
+ Logger.info(ctx, "PAYMENT_REJECTED", {"from": sender[:20], "reason": msg.reason})
1165
+
1166
+
1167
+ if not PAYMENT_PROTOCOL_AVAILABLE:
1168
+ @pay_proto.on_message(RequestPayment)
1169
+ async def handle_incoming_request(ctx: Context, sender: str, msg: RequestPayment) -> None:
1170
+ Logger.info(ctx, "PAYMENT_REQUEST_RECEIVED", {"from": sender[:20], "amount": msg.amount})
1171
+
1172
+
1173
+ # ==========================================================================
1174
+ # BACKGROUND TASKS
1175
+ # ==========================================================================
1176
+
1177
+
1178
+ @agent.on_interval(period=float(BUSINESS["interval_seconds"]))
1179
+ async def background_task(ctx: Context) -> None:
1180
+ """Periodic self-awareness update and health check."""
1181
+ self_aware.update(ctx)
1182
+ wallet.fund_check(ctx)
1183
+ ctx.logger.info(f"[HEALTH] {json.dumps(health.status())}")
1184
+
1185
+
1186
+ # ==========================================================================
1187
+ # WIRE UP
1188
+ # ==========================================================================
1189
+
1190
+ agent.include(chat_proto, publish_manifest=True)
1191
+ agent.include(pay_proto, publish_manifest=True)
1192
+
1193
+ if __name__ == "__main__":
1194
+ agent.run()
1195
+ `,
1196
+ };
1197
+ //# sourceMappingURL=genesis.js.map