@analytix402/openclaw-skill 0.1.0 → 0.1.2
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/SKILL.md +16 -5
- package/dist/index.js +13 -238
- package/dist/index.mjs +11 -235
- package/package.json +72 -3
- package/src/index.ts +0 -335
package/SKILL.md
CHANGED
|
@@ -16,16 +16,19 @@ Analytix402 gives your OpenClaw agent financial visibility and guardrails. Track
|
|
|
16
16
|
|
|
17
17
|
## Configuration
|
|
18
18
|
|
|
19
|
+
Add these to your OpenClaw agent's `.env` file:
|
|
20
|
+
|
|
19
21
|
```yaml
|
|
20
22
|
# Required
|
|
21
23
|
ANALYTIX402_API_KEY: ax_live_your_key_here
|
|
22
24
|
|
|
23
25
|
# Optional
|
|
24
|
-
ANALYTIX402_AGENT_ID: my-openclaw-agent
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
ANALYTIX402_AGENT_ID: my-openclaw-agent # Unique ID shown in dashboard
|
|
27
|
+
ANALYTIX402_AGENT_NAME: My OpenClaw Agent # Display name (defaults to agent ID)
|
|
28
|
+
ANALYTIX402_BASE_URL: https://analytix402.com # Custom endpoint
|
|
29
|
+
ANALYTIX402_DAILY_BUDGET: 50.00 # Daily spend limit in USD (0 = unlimited)
|
|
30
|
+
ANALYTIX402_PER_CALL_LIMIT: 5.00 # Per-call cap in USD (0 = unlimited)
|
|
31
|
+
ANALYTIX402_TRACK_LLM: true # LLM tracking (enabled by default)
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
## Tools
|
|
@@ -42,6 +45,14 @@ Check remaining budget before making an expensive API call.
|
|
|
42
45
|
### analytix402_flag_purchase
|
|
43
46
|
Flag a potential duplicate or unnecessary purchase for review.
|
|
44
47
|
|
|
48
|
+
## Verify it works
|
|
49
|
+
|
|
50
|
+
1. Start your OpenClaw agent — the skill connects automatically on load
|
|
51
|
+
2. Open the [Agents dashboard](https://analytix402.com/agents.html) — your agent should appear within seconds
|
|
52
|
+
3. The agent stays visible with a heartbeat every 60 seconds
|
|
53
|
+
|
|
54
|
+
No need to call any tools first. The skill registers itself immediately on startup.
|
|
55
|
+
|
|
45
56
|
## Tags
|
|
46
57
|
|
|
47
58
|
monitoring, analytics, budget, x402, payments, observability, cost-tracking
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -29,242 +30,7 @@ __export(index_exports, {
|
|
|
29
30
|
trackLLMCall: () => trackLLMCall
|
|
30
31
|
});
|
|
31
32
|
module.exports = __toCommonJS(index_exports);
|
|
32
|
-
|
|
33
|
-
// ../sdk/src/client.ts
|
|
34
|
-
var SDK_NAME = "@analytix402/sdk";
|
|
35
|
-
var SDK_VERSION = "0.1.1";
|
|
36
|
-
var DEFAULT_CONFIG = {
|
|
37
|
-
baseUrl: "https://analytix402.com",
|
|
38
|
-
debug: false,
|
|
39
|
-
batchSize: 100,
|
|
40
|
-
flushInterval: 5e3,
|
|
41
|
-
maxRetries: 3,
|
|
42
|
-
maxQueueSize: 1e3,
|
|
43
|
-
timeout: 1e4,
|
|
44
|
-
excludePaths: ["/health", "/healthz", "/ready", "/metrics", "/favicon.ico"]
|
|
45
|
-
};
|
|
46
|
-
function createClient(config) {
|
|
47
|
-
if (!config.apiKey) {
|
|
48
|
-
throw new Error("Analytix402: apiKey is required");
|
|
49
|
-
}
|
|
50
|
-
if (!config.apiKey.startsWith("ax_live_") && !config.apiKey.startsWith("ax_test_")) {
|
|
51
|
-
console.warn("Analytix402: API key should start with ax_live_ or ax_test_");
|
|
52
|
-
}
|
|
53
|
-
const cfg = {
|
|
54
|
-
...DEFAULT_CONFIG,
|
|
55
|
-
...config
|
|
56
|
-
};
|
|
57
|
-
const agentId = cfg.agentId;
|
|
58
|
-
const queue = [];
|
|
59
|
-
let flushTimer = null;
|
|
60
|
-
let isFlushing = false;
|
|
61
|
-
let isShutdown = false;
|
|
62
|
-
const log = (...args) => {
|
|
63
|
-
if (cfg.debug) {
|
|
64
|
-
console.log("[Analytix402]", ...args);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const warn = (...args) => {
|
|
68
|
-
console.warn("[Analytix402]", ...args);
|
|
69
|
-
};
|
|
70
|
-
function enqueue(event) {
|
|
71
|
-
if (isShutdown) {
|
|
72
|
-
warn("Client is shutdown, event dropped");
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
if (queue.length >= cfg.maxQueueSize) {
|
|
76
|
-
warn(`Queue full (${cfg.maxQueueSize}), dropping oldest event`);
|
|
77
|
-
queue.shift();
|
|
78
|
-
}
|
|
79
|
-
queue.push({
|
|
80
|
-
event,
|
|
81
|
-
attempts: 0,
|
|
82
|
-
addedAt: Date.now()
|
|
83
|
-
});
|
|
84
|
-
log(`Event queued (${queue.length} in queue)`);
|
|
85
|
-
if (queue.length >= cfg.batchSize) {
|
|
86
|
-
log("Batch size reached, flushing");
|
|
87
|
-
flush();
|
|
88
|
-
} else if (!flushTimer) {
|
|
89
|
-
flushTimer = setTimeout(() => {
|
|
90
|
-
flushTimer = null;
|
|
91
|
-
flush();
|
|
92
|
-
}, cfg.flushInterval);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function track(event) {
|
|
96
|
-
if (agentId && !event.agentId) {
|
|
97
|
-
event.agentId = agentId;
|
|
98
|
-
}
|
|
99
|
-
enqueue(event);
|
|
100
|
-
}
|
|
101
|
-
function heartbeat(status = "healthy", metadata) {
|
|
102
|
-
if (!agentId) {
|
|
103
|
-
warn("heartbeat() requires agentId in config");
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const event = {
|
|
107
|
-
type: "heartbeat",
|
|
108
|
-
agentId,
|
|
109
|
-
status,
|
|
110
|
-
metadata,
|
|
111
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
112
|
-
};
|
|
113
|
-
enqueue(event);
|
|
114
|
-
}
|
|
115
|
-
function reportOutcome(taskId, success, options) {
|
|
116
|
-
const event = {
|
|
117
|
-
type: "task_outcome",
|
|
118
|
-
agentId,
|
|
119
|
-
taskId,
|
|
120
|
-
success,
|
|
121
|
-
durationMs: options == null ? void 0 : options.durationMs,
|
|
122
|
-
cost: options == null ? void 0 : options.cost,
|
|
123
|
-
metadata: options == null ? void 0 : options.metadata,
|
|
124
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
125
|
-
};
|
|
126
|
-
enqueue(event);
|
|
127
|
-
}
|
|
128
|
-
function startTask(taskId) {
|
|
129
|
-
const id = taskId || `task_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
130
|
-
const startTime = Date.now();
|
|
131
|
-
return {
|
|
132
|
-
taskId: id,
|
|
133
|
-
end(success, metadata) {
|
|
134
|
-
const durationMs = Date.now() - startTime;
|
|
135
|
-
reportOutcome(id, success, { durationMs, metadata });
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
function trackLLM(usage) {
|
|
140
|
-
const event = {
|
|
141
|
-
type: "llm_usage",
|
|
142
|
-
agentId,
|
|
143
|
-
taskId: usage.taskId,
|
|
144
|
-
model: usage.model,
|
|
145
|
-
provider: usage.provider,
|
|
146
|
-
inputTokens: usage.inputTokens,
|
|
147
|
-
outputTokens: usage.outputTokens,
|
|
148
|
-
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
149
|
-
costUsd: usage.costUsd,
|
|
150
|
-
durationMs: usage.durationMs,
|
|
151
|
-
metadata: usage.metadata,
|
|
152
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
153
|
-
};
|
|
154
|
-
enqueue(event);
|
|
155
|
-
}
|
|
156
|
-
async function sendBatch(events) {
|
|
157
|
-
if (events.length === 0) return true;
|
|
158
|
-
const payload = {
|
|
159
|
-
events,
|
|
160
|
-
sdk: {
|
|
161
|
-
name: SDK_NAME,
|
|
162
|
-
version: SDK_VERSION
|
|
163
|
-
},
|
|
164
|
-
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
165
|
-
};
|
|
166
|
-
try {
|
|
167
|
-
const controller = new AbortController();
|
|
168
|
-
const timeoutId = setTimeout(() => controller.abort(), cfg.timeout);
|
|
169
|
-
const response = await fetch(`${cfg.baseUrl}/api/ingest/batch`, {
|
|
170
|
-
method: "POST",
|
|
171
|
-
headers: {
|
|
172
|
-
"Content-Type": "application/json",
|
|
173
|
-
"X-API-Key": cfg.apiKey,
|
|
174
|
-
"User-Agent": `${SDK_NAME}/${SDK_VERSION}`
|
|
175
|
-
},
|
|
176
|
-
body: JSON.stringify(payload),
|
|
177
|
-
signal: controller.signal
|
|
178
|
-
});
|
|
179
|
-
clearTimeout(timeoutId);
|
|
180
|
-
if (!response.ok) {
|
|
181
|
-
const text = await response.text().catch(() => "Unknown error");
|
|
182
|
-
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
183
|
-
}
|
|
184
|
-
log(`Sent ${events.length} events successfully`);
|
|
185
|
-
return true;
|
|
186
|
-
} catch (error) {
|
|
187
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
188
|
-
warn("Request timed out");
|
|
189
|
-
} else {
|
|
190
|
-
warn("Failed to send events:", error);
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
async function flush() {
|
|
196
|
-
if (isFlushing || queue.length === 0) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
isFlushing = true;
|
|
200
|
-
if (flushTimer) {
|
|
201
|
-
clearTimeout(flushTimer);
|
|
202
|
-
flushTimer = null;
|
|
203
|
-
}
|
|
204
|
-
const batch = queue.splice(0, cfg.batchSize);
|
|
205
|
-
const events = batch.map((q) => q.event);
|
|
206
|
-
log(`Flushing ${events.length} events`);
|
|
207
|
-
const success = await sendBatch(events);
|
|
208
|
-
if (!success) {
|
|
209
|
-
const retriable = batch.filter((q) => q.attempts < cfg.maxRetries);
|
|
210
|
-
if (retriable.length > 0) {
|
|
211
|
-
log(`Re-queuing ${retriable.length} events for retry`);
|
|
212
|
-
for (const item of retriable.reverse()) {
|
|
213
|
-
item.attempts++;
|
|
214
|
-
queue.unshift(item);
|
|
215
|
-
}
|
|
216
|
-
const backoff = Math.min(1e3 * Math.pow(2, retriable[0].attempts), 3e4);
|
|
217
|
-
log(`Retry scheduled in ${backoff}ms`);
|
|
218
|
-
flushTimer = setTimeout(() => {
|
|
219
|
-
flushTimer = null;
|
|
220
|
-
flush();
|
|
221
|
-
}, backoff);
|
|
222
|
-
} else {
|
|
223
|
-
warn(`Dropped ${batch.length} events after ${cfg.maxRetries} retries`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
isFlushing = false;
|
|
227
|
-
if (queue.length > 0 && !flushTimer) {
|
|
228
|
-
flushTimer = setTimeout(() => {
|
|
229
|
-
flushTimer = null;
|
|
230
|
-
flush();
|
|
231
|
-
}, cfg.flushInterval);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
async function shutdown2() {
|
|
235
|
-
log("Shutting down...");
|
|
236
|
-
isShutdown = true;
|
|
237
|
-
if (flushTimer) {
|
|
238
|
-
clearTimeout(flushTimer);
|
|
239
|
-
flushTimer = null;
|
|
240
|
-
}
|
|
241
|
-
if (queue.length > 0) {
|
|
242
|
-
log(`Flushing ${queue.length} remaining events`);
|
|
243
|
-
isFlushing = false;
|
|
244
|
-
await flush();
|
|
245
|
-
}
|
|
246
|
-
log("Shutdown complete");
|
|
247
|
-
}
|
|
248
|
-
if (typeof process !== "undefined") {
|
|
249
|
-
const handleExit = () => {
|
|
250
|
-
shutdown2().catch(console.error);
|
|
251
|
-
};
|
|
252
|
-
process.on("beforeExit", handleExit);
|
|
253
|
-
process.on("SIGINT", handleExit);
|
|
254
|
-
process.on("SIGTERM", handleExit);
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
track,
|
|
258
|
-
flush,
|
|
259
|
-
shutdown: shutdown2,
|
|
260
|
-
heartbeat,
|
|
261
|
-
reportOutcome,
|
|
262
|
-
startTask,
|
|
263
|
-
trackLLM
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// src/index.ts
|
|
33
|
+
var import_sdk = require("@analytix402/sdk");
|
|
268
34
|
var API_KEY = process.env.ANALYTIX402_API_KEY || "";
|
|
269
35
|
var BASE_URL = (process.env.ANALYTIX402_BASE_URL || "https://analytix402.com").replace(/\/$/, "");
|
|
270
36
|
var AGENT_ID = process.env.ANALYTIX402_AGENT_ID || `openclaw-${Date.now()}`;
|
|
@@ -284,15 +50,24 @@ function getClient() {
|
|
|
284
50
|
"ANALYTIX402_API_KEY is not set. Add it to your OpenClaw skill config."
|
|
285
51
|
);
|
|
286
52
|
}
|
|
287
|
-
client = createClient({
|
|
53
|
+
client = (0, import_sdk.createClient)({
|
|
288
54
|
apiKey: API_KEY,
|
|
289
55
|
baseUrl: BASE_URL,
|
|
290
56
|
agentId: AGENT_ID,
|
|
291
|
-
agentName: AGENT_NAME
|
|
57
|
+
agentName: AGENT_NAME,
|
|
58
|
+
autoConnect: true,
|
|
59
|
+
heartbeatIntervalMs: 6e4
|
|
292
60
|
});
|
|
293
61
|
}
|
|
294
62
|
return client;
|
|
295
63
|
}
|
|
64
|
+
if (API_KEY) {
|
|
65
|
+
try {
|
|
66
|
+
getClient();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.warn("[Analytix402] Failed to connect on startup:", err instanceof Error ? err.message : err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
296
71
|
async function apiRequest(method, path, body) {
|
|
297
72
|
const url = `${BASE_URL}/api${path}`;
|
|
298
73
|
const headers = {
|
package/dist/index.mjs
CHANGED
|
@@ -1,238 +1,5 @@
|
|
|
1
|
-
// ../sdk/src/client.ts
|
|
2
|
-
var SDK_NAME = "@analytix402/sdk";
|
|
3
|
-
var SDK_VERSION = "0.1.1";
|
|
4
|
-
var DEFAULT_CONFIG = {
|
|
5
|
-
baseUrl: "https://analytix402.com",
|
|
6
|
-
debug: false,
|
|
7
|
-
batchSize: 100,
|
|
8
|
-
flushInterval: 5e3,
|
|
9
|
-
maxRetries: 3,
|
|
10
|
-
maxQueueSize: 1e3,
|
|
11
|
-
timeout: 1e4,
|
|
12
|
-
excludePaths: ["/health", "/healthz", "/ready", "/metrics", "/favicon.ico"]
|
|
13
|
-
};
|
|
14
|
-
function createClient(config) {
|
|
15
|
-
if (!config.apiKey) {
|
|
16
|
-
throw new Error("Analytix402: apiKey is required");
|
|
17
|
-
}
|
|
18
|
-
if (!config.apiKey.startsWith("ax_live_") && !config.apiKey.startsWith("ax_test_")) {
|
|
19
|
-
console.warn("Analytix402: API key should start with ax_live_ or ax_test_");
|
|
20
|
-
}
|
|
21
|
-
const cfg = {
|
|
22
|
-
...DEFAULT_CONFIG,
|
|
23
|
-
...config
|
|
24
|
-
};
|
|
25
|
-
const agentId = cfg.agentId;
|
|
26
|
-
const queue = [];
|
|
27
|
-
let flushTimer = null;
|
|
28
|
-
let isFlushing = false;
|
|
29
|
-
let isShutdown = false;
|
|
30
|
-
const log = (...args) => {
|
|
31
|
-
if (cfg.debug) {
|
|
32
|
-
console.log("[Analytix402]", ...args);
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const warn = (...args) => {
|
|
36
|
-
console.warn("[Analytix402]", ...args);
|
|
37
|
-
};
|
|
38
|
-
function enqueue(event) {
|
|
39
|
-
if (isShutdown) {
|
|
40
|
-
warn("Client is shutdown, event dropped");
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
if (queue.length >= cfg.maxQueueSize) {
|
|
44
|
-
warn(`Queue full (${cfg.maxQueueSize}), dropping oldest event`);
|
|
45
|
-
queue.shift();
|
|
46
|
-
}
|
|
47
|
-
queue.push({
|
|
48
|
-
event,
|
|
49
|
-
attempts: 0,
|
|
50
|
-
addedAt: Date.now()
|
|
51
|
-
});
|
|
52
|
-
log(`Event queued (${queue.length} in queue)`);
|
|
53
|
-
if (queue.length >= cfg.batchSize) {
|
|
54
|
-
log("Batch size reached, flushing");
|
|
55
|
-
flush();
|
|
56
|
-
} else if (!flushTimer) {
|
|
57
|
-
flushTimer = setTimeout(() => {
|
|
58
|
-
flushTimer = null;
|
|
59
|
-
flush();
|
|
60
|
-
}, cfg.flushInterval);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function track(event) {
|
|
64
|
-
if (agentId && !event.agentId) {
|
|
65
|
-
event.agentId = agentId;
|
|
66
|
-
}
|
|
67
|
-
enqueue(event);
|
|
68
|
-
}
|
|
69
|
-
function heartbeat(status = "healthy", metadata) {
|
|
70
|
-
if (!agentId) {
|
|
71
|
-
warn("heartbeat() requires agentId in config");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const event = {
|
|
75
|
-
type: "heartbeat",
|
|
76
|
-
agentId,
|
|
77
|
-
status,
|
|
78
|
-
metadata,
|
|
79
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
80
|
-
};
|
|
81
|
-
enqueue(event);
|
|
82
|
-
}
|
|
83
|
-
function reportOutcome(taskId, success, options) {
|
|
84
|
-
const event = {
|
|
85
|
-
type: "task_outcome",
|
|
86
|
-
agentId,
|
|
87
|
-
taskId,
|
|
88
|
-
success,
|
|
89
|
-
durationMs: options == null ? void 0 : options.durationMs,
|
|
90
|
-
cost: options == null ? void 0 : options.cost,
|
|
91
|
-
metadata: options == null ? void 0 : options.metadata,
|
|
92
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
93
|
-
};
|
|
94
|
-
enqueue(event);
|
|
95
|
-
}
|
|
96
|
-
function startTask(taskId) {
|
|
97
|
-
const id = taskId || `task_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
98
|
-
const startTime = Date.now();
|
|
99
|
-
return {
|
|
100
|
-
taskId: id,
|
|
101
|
-
end(success, metadata) {
|
|
102
|
-
const durationMs = Date.now() - startTime;
|
|
103
|
-
reportOutcome(id, success, { durationMs, metadata });
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
function trackLLM(usage) {
|
|
108
|
-
const event = {
|
|
109
|
-
type: "llm_usage",
|
|
110
|
-
agentId,
|
|
111
|
-
taskId: usage.taskId,
|
|
112
|
-
model: usage.model,
|
|
113
|
-
provider: usage.provider,
|
|
114
|
-
inputTokens: usage.inputTokens,
|
|
115
|
-
outputTokens: usage.outputTokens,
|
|
116
|
-
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
117
|
-
costUsd: usage.costUsd,
|
|
118
|
-
durationMs: usage.durationMs,
|
|
119
|
-
metadata: usage.metadata,
|
|
120
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
121
|
-
};
|
|
122
|
-
enqueue(event);
|
|
123
|
-
}
|
|
124
|
-
async function sendBatch(events) {
|
|
125
|
-
if (events.length === 0) return true;
|
|
126
|
-
const payload = {
|
|
127
|
-
events,
|
|
128
|
-
sdk: {
|
|
129
|
-
name: SDK_NAME,
|
|
130
|
-
version: SDK_VERSION
|
|
131
|
-
},
|
|
132
|
-
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
133
|
-
};
|
|
134
|
-
try {
|
|
135
|
-
const controller = new AbortController();
|
|
136
|
-
const timeoutId = setTimeout(() => controller.abort(), cfg.timeout);
|
|
137
|
-
const response = await fetch(`${cfg.baseUrl}/api/ingest/batch`, {
|
|
138
|
-
method: "POST",
|
|
139
|
-
headers: {
|
|
140
|
-
"Content-Type": "application/json",
|
|
141
|
-
"X-API-Key": cfg.apiKey,
|
|
142
|
-
"User-Agent": `${SDK_NAME}/${SDK_VERSION}`
|
|
143
|
-
},
|
|
144
|
-
body: JSON.stringify(payload),
|
|
145
|
-
signal: controller.signal
|
|
146
|
-
});
|
|
147
|
-
clearTimeout(timeoutId);
|
|
148
|
-
if (!response.ok) {
|
|
149
|
-
const text = await response.text().catch(() => "Unknown error");
|
|
150
|
-
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
151
|
-
}
|
|
152
|
-
log(`Sent ${events.length} events successfully`);
|
|
153
|
-
return true;
|
|
154
|
-
} catch (error) {
|
|
155
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
156
|
-
warn("Request timed out");
|
|
157
|
-
} else {
|
|
158
|
-
warn("Failed to send events:", error);
|
|
159
|
-
}
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
async function flush() {
|
|
164
|
-
if (isFlushing || queue.length === 0) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
isFlushing = true;
|
|
168
|
-
if (flushTimer) {
|
|
169
|
-
clearTimeout(flushTimer);
|
|
170
|
-
flushTimer = null;
|
|
171
|
-
}
|
|
172
|
-
const batch = queue.splice(0, cfg.batchSize);
|
|
173
|
-
const events = batch.map((q) => q.event);
|
|
174
|
-
log(`Flushing ${events.length} events`);
|
|
175
|
-
const success = await sendBatch(events);
|
|
176
|
-
if (!success) {
|
|
177
|
-
const retriable = batch.filter((q) => q.attempts < cfg.maxRetries);
|
|
178
|
-
if (retriable.length > 0) {
|
|
179
|
-
log(`Re-queuing ${retriable.length} events for retry`);
|
|
180
|
-
for (const item of retriable.reverse()) {
|
|
181
|
-
item.attempts++;
|
|
182
|
-
queue.unshift(item);
|
|
183
|
-
}
|
|
184
|
-
const backoff = Math.min(1e3 * Math.pow(2, retriable[0].attempts), 3e4);
|
|
185
|
-
log(`Retry scheduled in ${backoff}ms`);
|
|
186
|
-
flushTimer = setTimeout(() => {
|
|
187
|
-
flushTimer = null;
|
|
188
|
-
flush();
|
|
189
|
-
}, backoff);
|
|
190
|
-
} else {
|
|
191
|
-
warn(`Dropped ${batch.length} events after ${cfg.maxRetries} retries`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
isFlushing = false;
|
|
195
|
-
if (queue.length > 0 && !flushTimer) {
|
|
196
|
-
flushTimer = setTimeout(() => {
|
|
197
|
-
flushTimer = null;
|
|
198
|
-
flush();
|
|
199
|
-
}, cfg.flushInterval);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
async function shutdown2() {
|
|
203
|
-
log("Shutting down...");
|
|
204
|
-
isShutdown = true;
|
|
205
|
-
if (flushTimer) {
|
|
206
|
-
clearTimeout(flushTimer);
|
|
207
|
-
flushTimer = null;
|
|
208
|
-
}
|
|
209
|
-
if (queue.length > 0) {
|
|
210
|
-
log(`Flushing ${queue.length} remaining events`);
|
|
211
|
-
isFlushing = false;
|
|
212
|
-
await flush();
|
|
213
|
-
}
|
|
214
|
-
log("Shutdown complete");
|
|
215
|
-
}
|
|
216
|
-
if (typeof process !== "undefined") {
|
|
217
|
-
const handleExit = () => {
|
|
218
|
-
shutdown2().catch(console.error);
|
|
219
|
-
};
|
|
220
|
-
process.on("beforeExit", handleExit);
|
|
221
|
-
process.on("SIGINT", handleExit);
|
|
222
|
-
process.on("SIGTERM", handleExit);
|
|
223
|
-
}
|
|
224
|
-
return {
|
|
225
|
-
track,
|
|
226
|
-
flush,
|
|
227
|
-
shutdown: shutdown2,
|
|
228
|
-
heartbeat,
|
|
229
|
-
reportOutcome,
|
|
230
|
-
startTask,
|
|
231
|
-
trackLLM
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
1
|
// src/index.ts
|
|
2
|
+
import { createClient } from "@analytix402/sdk";
|
|
236
3
|
var API_KEY = process.env.ANALYTIX402_API_KEY || "";
|
|
237
4
|
var BASE_URL = (process.env.ANALYTIX402_BASE_URL || "https://analytix402.com").replace(/\/$/, "");
|
|
238
5
|
var AGENT_ID = process.env.ANALYTIX402_AGENT_ID || `openclaw-${Date.now()}`;
|
|
@@ -256,11 +23,20 @@ function getClient() {
|
|
|
256
23
|
apiKey: API_KEY,
|
|
257
24
|
baseUrl: BASE_URL,
|
|
258
25
|
agentId: AGENT_ID,
|
|
259
|
-
agentName: AGENT_NAME
|
|
26
|
+
agentName: AGENT_NAME,
|
|
27
|
+
autoConnect: true,
|
|
28
|
+
heartbeatIntervalMs: 6e4
|
|
260
29
|
});
|
|
261
30
|
}
|
|
262
31
|
return client;
|
|
263
32
|
}
|
|
33
|
+
if (API_KEY) {
|
|
34
|
+
try {
|
|
35
|
+
getClient();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn("[Analytix402] Failed to connect on startup:", err instanceof Error ? err.message : err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
264
40
|
async function apiRequest(method, path, body) {
|
|
265
41
|
const url = `${BASE_URL}/api${path}`;
|
|
266
42
|
const headers = {
|
package/package.json
CHANGED
|
@@ -1,16 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analytix402/openclaw-skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Analytix402 skill for OpenClaw — monitor, control, and optimize your AI agent's spend",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
7
15
|
"scripts": {
|
|
8
16
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
9
17
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
10
|
-
"typecheck": "tsc --noEmit"
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"openclaw": {
|
|
22
|
+
"tools": {
|
|
23
|
+
"analytix402_spend_report": {
|
|
24
|
+
"description": "Get a summary of your agent's spend — total cost, breakdown by API and LLM provider, and efficiency score.",
|
|
25
|
+
"parameters": {}
|
|
26
|
+
},
|
|
27
|
+
"analytix402_set_budget": {
|
|
28
|
+
"description": "Set or update the daily budget limit for this agent session.",
|
|
29
|
+
"parameters": {
|
|
30
|
+
"daily_limit": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Maximum daily spend in USD (0 = unlimited)"
|
|
33
|
+
},
|
|
34
|
+
"per_call_limit": {
|
|
35
|
+
"type": "number",
|
|
36
|
+
"description": "Maximum spend per single API call in USD (0 = unlimited)"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"analytix402_check_budget": {
|
|
41
|
+
"description": "Check remaining budget before making an expensive API call.",
|
|
42
|
+
"parameters": {
|
|
43
|
+
"estimated_cost": {
|
|
44
|
+
"type": "number",
|
|
45
|
+
"description": "The estimated cost in USD for the upcoming API call"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"analytix402_flag_purchase": {
|
|
50
|
+
"description": "Flag a potential duplicate or unnecessary purchase for review.",
|
|
51
|
+
"parameters": {
|
|
52
|
+
"url": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "The URL of the API call to flag",
|
|
55
|
+
"required": true
|
|
56
|
+
},
|
|
57
|
+
"reason": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Why this purchase is being flagged"
|
|
60
|
+
},
|
|
61
|
+
"estimated_cost": {
|
|
62
|
+
"type": "number",
|
|
63
|
+
"description": "Estimated cost of the flagged purchase in USD"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"hooks": [
|
|
69
|
+
"trackLLMCall",
|
|
70
|
+
"trackAPICall",
|
|
71
|
+
"sendHeartbeat",
|
|
72
|
+
"shutdown"
|
|
73
|
+
]
|
|
11
74
|
},
|
|
75
|
+
"files": [
|
|
76
|
+
"dist",
|
|
77
|
+
"SKILL.md",
|
|
78
|
+
"README.md"
|
|
79
|
+
],
|
|
12
80
|
"keywords": [
|
|
13
81
|
"openclaw",
|
|
82
|
+
"openclaw-skill",
|
|
14
83
|
"analytix402",
|
|
15
84
|
"x402",
|
|
16
85
|
"ai-agent",
|
|
@@ -22,7 +91,7 @@
|
|
|
22
91
|
"author": "Analytix402",
|
|
23
92
|
"license": "MIT",
|
|
24
93
|
"dependencies": {
|
|
25
|
-
"@analytix402/sdk": "^0.1.
|
|
94
|
+
"@analytix402/sdk": "^0.1.2"
|
|
26
95
|
},
|
|
27
96
|
"devDependencies": {
|
|
28
97
|
"@types/node": "^20.10.0",
|
package/src/index.ts
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analytix402 OpenClaw Skill
|
|
3
|
-
*
|
|
4
|
-
* Provides AI agent spend tracking, budget enforcement,
|
|
5
|
-
* and observability for OpenClaw agents.
|
|
6
|
-
*
|
|
7
|
-
* Integrates with the Analytix402 platform to give operators
|
|
8
|
-
* real-time visibility into what their agents are spending.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createClient } from '../../sdk/src/client';
|
|
12
|
-
import type { Analytix402Client } from '../../sdk/src/types';
|
|
13
|
-
|
|
14
|
-
// ============================================================
|
|
15
|
-
// Configuration
|
|
16
|
-
// ============================================================
|
|
17
|
-
|
|
18
|
-
const API_KEY = process.env.ANALYTIX402_API_KEY || '';
|
|
19
|
-
const BASE_URL = (process.env.ANALYTIX402_BASE_URL || 'https://analytix402.com').replace(/\/$/, '');
|
|
20
|
-
const AGENT_ID = process.env.ANALYTIX402_AGENT_ID || `openclaw-${Date.now()}`;
|
|
21
|
-
const AGENT_NAME = process.env.ANALYTIX402_AGENT_NAME || 'OpenClaw Agent';
|
|
22
|
-
const DAILY_BUDGET = parseFloat(process.env.ANALYTIX402_DAILY_BUDGET || '0') || 0;
|
|
23
|
-
const PER_CALL_LIMIT = parseFloat(process.env.ANALYTIX402_PER_CALL_LIMIT || '0') || 0;
|
|
24
|
-
const TRACK_LLM = process.env.ANALYTIX402_TRACK_LLM !== 'false';
|
|
25
|
-
|
|
26
|
-
// ============================================================
|
|
27
|
-
// State
|
|
28
|
-
// ============================================================
|
|
29
|
-
|
|
30
|
-
let client: Analytix402Client | null = null;
|
|
31
|
-
let sessionSpend = 0;
|
|
32
|
-
let sessionCalls = 0;
|
|
33
|
-
let dailySpend = 0;
|
|
34
|
-
const dailySpendDate = new Date().toISOString().slice(0, 10);
|
|
35
|
-
|
|
36
|
-
function getClient(): Analytix402Client {
|
|
37
|
-
if (!client) {
|
|
38
|
-
if (!API_KEY) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
'ANALYTIX402_API_KEY is not set. Add it to your OpenClaw skill config.'
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
client = createClient({
|
|
44
|
-
apiKey: API_KEY,
|
|
45
|
-
baseUrl: BASE_URL,
|
|
46
|
-
agentId: AGENT_ID,
|
|
47
|
-
agentName: AGENT_NAME,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
return client;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ============================================================
|
|
54
|
-
// API helper (for dashboard queries)
|
|
55
|
-
// ============================================================
|
|
56
|
-
|
|
57
|
-
async function apiRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
58
|
-
const url = `${BASE_URL}/api${path}`;
|
|
59
|
-
const headers: Record<string, string> = {
|
|
60
|
-
'Content-Type': 'application/json',
|
|
61
|
-
'Authorization': `Bearer ${API_KEY}`,
|
|
62
|
-
'X-API-Key': API_KEY,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const opts: RequestInit = { method, headers };
|
|
66
|
-
if (body && method !== 'GET') {
|
|
67
|
-
opts.body = JSON.stringify(body);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const res = await fetch(url, opts);
|
|
71
|
-
const text = await res.text();
|
|
72
|
-
|
|
73
|
-
let data;
|
|
74
|
-
try {
|
|
75
|
-
data = JSON.parse(text);
|
|
76
|
-
} catch {
|
|
77
|
-
data = { raw: text };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!res.ok) {
|
|
81
|
-
throw new Error(`Analytix402 API ${res.status}: ${(data as Record<string, string>).error || text}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return data;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ============================================================
|
|
88
|
-
// Tool: analytix402_spend_report
|
|
89
|
-
// ============================================================
|
|
90
|
-
|
|
91
|
-
export async function analytix402_spend_report(): Promise<string> {
|
|
92
|
-
const c = getClient();
|
|
93
|
-
|
|
94
|
-
// Send a heartbeat while we're at it
|
|
95
|
-
c.heartbeat('healthy', { sessionCalls, sessionSpend });
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
const overview = await apiRequest('GET', '/agents/overview');
|
|
99
|
-
const insights = await apiRequest('GET', '/agents/insights');
|
|
100
|
-
|
|
101
|
-
return JSON.stringify({
|
|
102
|
-
session: {
|
|
103
|
-
totalCalls: sessionCalls,
|
|
104
|
-
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
105
|
-
dailySpend: `$${dailySpend.toFixed(4)}`,
|
|
106
|
-
dailyBudget: DAILY_BUDGET > 0 ? `$${DAILY_BUDGET.toFixed(2)}` : 'unlimited',
|
|
107
|
-
budgetRemaining: DAILY_BUDGET > 0
|
|
108
|
-
? `$${(DAILY_BUDGET - dailySpend).toFixed(4)}`
|
|
109
|
-
: 'unlimited',
|
|
110
|
-
},
|
|
111
|
-
platform: overview,
|
|
112
|
-
insights,
|
|
113
|
-
}, null, 2);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
return JSON.stringify({
|
|
116
|
-
session: {
|
|
117
|
-
totalCalls: sessionCalls,
|
|
118
|
-
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
119
|
-
dailySpend: `$${dailySpend.toFixed(4)}`,
|
|
120
|
-
dailyBudget: DAILY_BUDGET > 0 ? `$${DAILY_BUDGET.toFixed(2)}` : 'unlimited',
|
|
121
|
-
},
|
|
122
|
-
error: error instanceof Error ? error.message : String(error),
|
|
123
|
-
}, null, 2);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ============================================================
|
|
128
|
-
// Tool: analytix402_set_budget
|
|
129
|
-
// ============================================================
|
|
130
|
-
|
|
131
|
-
export interface SetBudgetParams {
|
|
132
|
-
daily_limit?: number;
|
|
133
|
-
per_call_limit?: number;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function analytix402_set_budget(params: SetBudgetParams): string {
|
|
137
|
-
const updates: string[] = [];
|
|
138
|
-
|
|
139
|
-
if (params.daily_limit !== undefined && params.daily_limit >= 0) {
|
|
140
|
-
// Update the runtime budget (persists for this session)
|
|
141
|
-
Object.defineProperty(globalThis, '__ax402_daily_budget', {
|
|
142
|
-
value: params.daily_limit,
|
|
143
|
-
writable: true,
|
|
144
|
-
configurable: true,
|
|
145
|
-
});
|
|
146
|
-
updates.push(`Daily budget set to $${params.daily_limit.toFixed(2)}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (params.per_call_limit !== undefined && params.per_call_limit >= 0) {
|
|
150
|
-
Object.defineProperty(globalThis, '__ax402_per_call_limit', {
|
|
151
|
-
value: params.per_call_limit,
|
|
152
|
-
writable: true,
|
|
153
|
-
configurable: true,
|
|
154
|
-
});
|
|
155
|
-
updates.push(`Per-call limit set to $${params.per_call_limit.toFixed(2)}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (updates.length === 0) {
|
|
159
|
-
return 'No budget parameters provided. Use daily_limit and/or per_call_limit.';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return updates.join('. ') + '.';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ============================================================
|
|
166
|
-
// Tool: analytix402_check_budget
|
|
167
|
-
// ============================================================
|
|
168
|
-
|
|
169
|
-
export interface CheckBudgetParams {
|
|
170
|
-
estimated_cost?: number;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function analytix402_check_budget(params: CheckBudgetParams): string {
|
|
174
|
-
const budget = DAILY_BUDGET ||
|
|
175
|
-
((globalThis as Record<string, unknown>).__ax402_daily_budget as number) || 0;
|
|
176
|
-
const callLimit = PER_CALL_LIMIT ||
|
|
177
|
-
((globalThis as Record<string, unknown>).__ax402_per_call_limit as number) || 0;
|
|
178
|
-
const estimated = params.estimated_cost || 0;
|
|
179
|
-
|
|
180
|
-
const remaining = budget > 0 ? budget - dailySpend : Infinity;
|
|
181
|
-
const allowed = (budget === 0 || remaining >= estimated) &&
|
|
182
|
-
(callLimit === 0 || estimated <= callLimit);
|
|
183
|
-
|
|
184
|
-
return JSON.stringify({
|
|
185
|
-
allowed,
|
|
186
|
-
dailyBudget: budget > 0 ? `$${budget.toFixed(2)}` : 'unlimited',
|
|
187
|
-
dailySpent: `$${dailySpend.toFixed(4)}`,
|
|
188
|
-
remaining: budget > 0 ? `$${remaining.toFixed(4)}` : 'unlimited',
|
|
189
|
-
estimatedCost: `$${estimated.toFixed(4)}`,
|
|
190
|
-
perCallLimit: callLimit > 0 ? `$${callLimit.toFixed(2)}` : 'unlimited',
|
|
191
|
-
withinPerCallLimit: callLimit === 0 || estimated <= callLimit,
|
|
192
|
-
withinDailyBudget: budget === 0 || remaining >= estimated,
|
|
193
|
-
}, null, 2);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ============================================================
|
|
197
|
-
// Tool: analytix402_flag_purchase
|
|
198
|
-
// ============================================================
|
|
199
|
-
|
|
200
|
-
export interface FlagPurchaseParams {
|
|
201
|
-
url: string;
|
|
202
|
-
reason?: string;
|
|
203
|
-
estimated_cost?: number;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function analytix402_flag_purchase(params: FlagPurchaseParams): string {
|
|
207
|
-
const c = getClient();
|
|
208
|
-
|
|
209
|
-
c.track({
|
|
210
|
-
type: 'request',
|
|
211
|
-
method: 'FLAG',
|
|
212
|
-
path: params.url,
|
|
213
|
-
endpoint: params.url,
|
|
214
|
-
statusCode: 0,
|
|
215
|
-
responseTimeMs: 0,
|
|
216
|
-
timestamp: new Date().toISOString(),
|
|
217
|
-
agentId: AGENT_ID,
|
|
218
|
-
metadata: {
|
|
219
|
-
flagged: true,
|
|
220
|
-
reason: params.reason || 'Potential duplicate or unnecessary purchase',
|
|
221
|
-
estimatedCost: params.estimated_cost,
|
|
222
|
-
},
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return `Flagged: ${params.url} — ${params.reason || 'potential duplicate purchase'}. This will appear in your Analytix402 dashboard for review.`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ============================================================
|
|
229
|
-
// Hooks: auto-track LLM and API calls
|
|
230
|
-
// ============================================================
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Call this after any LLM invocation to track usage.
|
|
234
|
-
* OpenClaw skills can hook into the agent lifecycle.
|
|
235
|
-
*/
|
|
236
|
-
export function trackLLMCall(params: {
|
|
237
|
-
model: string;
|
|
238
|
-
provider: string;
|
|
239
|
-
inputTokens: number;
|
|
240
|
-
outputTokens: number;
|
|
241
|
-
costUsd?: number;
|
|
242
|
-
durationMs?: number;
|
|
243
|
-
taskId?: string;
|
|
244
|
-
}): void {
|
|
245
|
-
if (!TRACK_LLM) return;
|
|
246
|
-
|
|
247
|
-
const c = getClient();
|
|
248
|
-
const cost = params.costUsd || 0;
|
|
249
|
-
|
|
250
|
-
c.trackLLM({
|
|
251
|
-
model: params.model,
|
|
252
|
-
provider: params.provider,
|
|
253
|
-
inputTokens: params.inputTokens,
|
|
254
|
-
outputTokens: params.outputTokens,
|
|
255
|
-
costUsd: params.costUsd,
|
|
256
|
-
durationMs: params.durationMs,
|
|
257
|
-
taskId: params.taskId,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
sessionSpend += cost;
|
|
261
|
-
sessionCalls += 1;
|
|
262
|
-
|
|
263
|
-
// Reset daily spend if date changed
|
|
264
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
265
|
-
if (today !== dailySpendDate) {
|
|
266
|
-
dailySpend = 0;
|
|
267
|
-
}
|
|
268
|
-
dailySpend += cost;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Call this after any API call to track spend.
|
|
273
|
-
*/
|
|
274
|
-
export function trackAPICall(params: {
|
|
275
|
-
method: string;
|
|
276
|
-
url: string;
|
|
277
|
-
statusCode: number;
|
|
278
|
-
responseTimeMs: number;
|
|
279
|
-
paymentAmount?: number;
|
|
280
|
-
paymentCurrency?: string;
|
|
281
|
-
txHash?: string;
|
|
282
|
-
}): void {
|
|
283
|
-
const c = getClient();
|
|
284
|
-
|
|
285
|
-
const payment = params.paymentAmount
|
|
286
|
-
? {
|
|
287
|
-
amount: String(params.paymentAmount),
|
|
288
|
-
currency: params.paymentCurrency || 'USDC',
|
|
289
|
-
wallet: '',
|
|
290
|
-
status: 'success' as const,
|
|
291
|
-
txHash: params.txHash,
|
|
292
|
-
}
|
|
293
|
-
: undefined;
|
|
294
|
-
|
|
295
|
-
c.track({
|
|
296
|
-
type: 'request',
|
|
297
|
-
method: params.method,
|
|
298
|
-
path: params.url,
|
|
299
|
-
endpoint: params.url,
|
|
300
|
-
statusCode: params.statusCode,
|
|
301
|
-
responseTimeMs: params.responseTimeMs,
|
|
302
|
-
timestamp: new Date().toISOString(),
|
|
303
|
-
agentId: AGENT_ID,
|
|
304
|
-
payment,
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
if (params.paymentAmount) {
|
|
308
|
-
sessionSpend += params.paymentAmount;
|
|
309
|
-
dailySpend += params.paymentAmount;
|
|
310
|
-
}
|
|
311
|
-
sessionCalls += 1;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Send a heartbeat — call periodically to show agent is alive.
|
|
316
|
-
*/
|
|
317
|
-
export function sendHeartbeat(status: 'healthy' | 'degraded' | 'error' = 'healthy'): void {
|
|
318
|
-
const c = getClient();
|
|
319
|
-
c.heartbeat(status, {
|
|
320
|
-
sessionCalls,
|
|
321
|
-
sessionSpend,
|
|
322
|
-
dailySpend,
|
|
323
|
-
uptime: process.uptime(),
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Gracefully shutdown — flush all pending events.
|
|
329
|
-
*/
|
|
330
|
-
export async function shutdown(): Promise<void> {
|
|
331
|
-
if (client) {
|
|
332
|
-
await client.shutdown();
|
|
333
|
-
client = null;
|
|
334
|
-
}
|
|
335
|
-
}
|