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.
- package/dist/__tests__/build.test.d.ts +1 -1
- package/dist/__tests__/build.test.js +5 -5
- package/dist/__tests__/build.test.js.map +1 -1
- package/dist/__tests__/consumer-commerce.test.d.ts +11 -0
- package/dist/__tests__/consumer-commerce.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-commerce.test.js +118 -0
- package/dist/__tests__/consumer-commerce.test.js.map +1 -0
- package/dist/__tests__/swarm-starter-integration.test.d.ts +12 -0
- package/dist/__tests__/swarm-starter-integration.test.d.ts.map +1 -0
- package/dist/__tests__/swarm-starter-integration.test.js +143 -0
- package/dist/__tests__/swarm-starter-integration.test.js.map +1 -0
- package/dist/__tests__/swarm-starter.test.d.ts +16 -0
- package/dist/__tests__/swarm-starter.test.d.ts.map +1 -0
- package/dist/__tests__/swarm-starter.test.js +310 -0
- package/dist/__tests__/swarm-starter.test.js.map +1 -0
- package/dist/claude-context.d.ts +1 -1
- package/dist/claude-context.d.ts.map +1 -1
- package/dist/claude-context.js +55 -49
- package/dist/claude-context.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/people.d.ts +108 -0
- package/dist/people.d.ts.map +1 -0
- package/dist/people.js +563 -0
- package/dist/people.js.map +1 -0
- package/dist/presets.d.ts +13 -13
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +331 -96
- package/dist/presets.js.map +1 -1
- package/dist/registry.d.ts +3 -8
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +8 -28
- package/dist/registry.js.map +1 -1
- package/dist/templates/chat-memory.d.ts +5 -14
- package/dist/templates/chat-memory.d.ts.map +1 -1
- package/dist/templates/chat-memory.js +142 -220
- package/dist/templates/chat-memory.js.map +1 -1
- package/dist/templates/consumer-commerce.d.ts +14 -0
- package/dist/templates/consumer-commerce.d.ts.map +1 -0
- package/dist/templates/consumer-commerce.js +439 -0
- package/dist/templates/consumer-commerce.js.map +1 -0
- package/dist/templates/genesis.d.ts.map +1 -1
- package/dist/templates/genesis.js +10 -0
- package/dist/templates/genesis.js.map +1 -1
- package/dist/templates/swarm-starter.d.ts +26 -0
- package/dist/templates/swarm-starter.d.ts.map +1 -0
- package/dist/templates/swarm-starter.js +1421 -0
- package/dist/templates/swarm-starter.js.map +1 -0
- 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
|