@x402sentinel/x402 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +388 -0
- package/dist/chunk-25PZCEL2.js +93 -0
- package/dist/chunk-25PZCEL2.js.map +1 -0
- package/dist/chunk-7H4FRU7K.cjs +103 -0
- package/dist/chunk-7H4FRU7K.cjs.map +1 -0
- package/dist/dashboard.cjs +178 -0
- package/dist/dashboard.cjs.map +1 -0
- package/dist/dashboard.d.cts +95 -0
- package/dist/dashboard.d.ts +95 -0
- package/dist/dashboard.js +170 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/index.cjs +1091 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +387 -0
- package/dist/index.d.ts +387 -0
- package/dist/index.js +1074 -0
- package/dist/index.js.map +1 -0
- package/dist/interface-CNi4rtm1.d.cts +110 -0
- package/dist/interface-CNi4rtm1.d.ts +110 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Valeo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Valeo Sentinel
|
|
2
|
+
|
|
3
|
+
**Enterprise audit & compliance for x402 payments. One line to add. Zero config to start.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
AI agents are spending real money autonomously. The [x402 protocol](https://github.com/coinbase/x402) enables internet-native payments, but provides no built-in audit trail, budget controls, or compliance tooling.
|
|
10
|
+
|
|
11
|
+
- 75% of enterprise leaders say compliance is a blocker for autonomous agent payments (KPMG 2025)
|
|
12
|
+
- 61% report fragmented payment logs across agent fleets
|
|
13
|
+
- Zero visibility into *which agent* spent *how much* on *what endpoint* and *who approved it*
|
|
14
|
+
|
|
15
|
+
Sentinel fixes this with a single line of code.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Before (plain x402)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
|
|
25
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
26
|
+
|
|
27
|
+
const client = new x402Client();
|
|
28
|
+
registerExactEvmScheme(client, { signer });
|
|
29
|
+
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
|
|
30
|
+
|
|
31
|
+
const response = await fetchWithPayment("https://api.example.com/paid");
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### After (with Sentinel)
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { x402Client, wrapFetchWithPayment } from "@x402/fetch";
|
|
38
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
39
|
+
import { wrapWithSentinel, standardPolicy } from "@x402sentinel/x402"; // <-- add
|
|
40
|
+
|
|
41
|
+
const client = new x402Client();
|
|
42
|
+
registerExactEvmScheme(client, { signer });
|
|
43
|
+
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
|
|
44
|
+
|
|
45
|
+
const secureFetch = wrapWithSentinel(fetchWithPayment, { // <-- wrap
|
|
46
|
+
agentId: "agent-weather-001",
|
|
47
|
+
budget: standardPolicy(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const response = await secureFetch("https://api.example.com/paid"); // same API
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
That's it. Same `fetch` interface. Budget enforcement + audit trail included.
|
|
54
|
+
|
|
55
|
+
### Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @x402sentinel/x402
|
|
59
|
+
# or
|
|
60
|
+
pnpm add @x402sentinel/x402
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Features
|
|
66
|
+
|
|
67
|
+
| Feature | Description |
|
|
68
|
+
|---------|-------------|
|
|
69
|
+
| **Budget Enforcement** | Per-call, hourly, daily, and lifetime spend limits. Blocks before payment. |
|
|
70
|
+
| **Spike Detection** | Flags payments that exceed N× the rolling average |
|
|
71
|
+
| **Audit Trails** | Every payment logged with agent ID, team, endpoint, tx hash, timing |
|
|
72
|
+
| **Endpoint Filtering** | Allowlist/blocklist URL patterns |
|
|
73
|
+
| **Approval Workflows** | Require human approval above a threshold |
|
|
74
|
+
| **Storage Backends** | In-memory (default), JSONL file, or remote API |
|
|
75
|
+
| **Dashboard Queries** | Query spend by agent, team, endpoint, time range |
|
|
76
|
+
| **CSV/JSON Export** | Export audit data for compliance reviews |
|
|
77
|
+
| **Zero Dependencies** | No runtime deps beyond x402 peer deps |
|
|
78
|
+
| **Drop-in Wrapper** | One line change. Remove Sentinel, code works identically. |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const secureFetch = wrapWithSentinel(fetchWithPayment, {
|
|
86
|
+
// Required
|
|
87
|
+
agentId: "agent-weather-001",
|
|
88
|
+
|
|
89
|
+
// Optional identity
|
|
90
|
+
team: "data-ops",
|
|
91
|
+
humanSponsor: "alice@company.com",
|
|
92
|
+
|
|
93
|
+
// Budget policy
|
|
94
|
+
budget: {
|
|
95
|
+
maxPerCall: "1.00", // max USDC per single payment
|
|
96
|
+
maxPerHour: "25.00", // hourly rolling cap
|
|
97
|
+
maxPerDay: "200.00", // daily rolling cap
|
|
98
|
+
maxTotal: "10000.00", // lifetime cap
|
|
99
|
+
spikeThreshold: 3.0, // flag if > 3× rolling average
|
|
100
|
+
allowedEndpoints: ["https://api.trusted.com/*"],
|
|
101
|
+
blockedEndpoints: ["https://api.sketchy.com/*"],
|
|
102
|
+
requireApproval: {
|
|
103
|
+
above: "50.00",
|
|
104
|
+
handler: async (ctx) => await askSlackForApproval(ctx),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Audit settings
|
|
109
|
+
audit: {
|
|
110
|
+
enabled: true,
|
|
111
|
+
storage: new MemoryStorage(), // or FileStorage, ApiStorage
|
|
112
|
+
redactFields: ["secret_key"],
|
|
113
|
+
enrichment: {
|
|
114
|
+
staticTags: ["production"],
|
|
115
|
+
tagRules: [
|
|
116
|
+
{ pattern: ".*openai.*", tags: ["llm", "openai"] },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Lifecycle hooks
|
|
122
|
+
hooks: {
|
|
123
|
+
afterPayment: async (record) => { /* log to DataDog */ },
|
|
124
|
+
onBudgetExceeded: async (violation) => { /* page on-call */ },
|
|
125
|
+
onAnomaly: async (anomaly) => { /* alert Slack */ },
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Custom metadata on every record
|
|
129
|
+
metadata: { environment: "production", cost_center: "ENG-2024" },
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Budget Policies
|
|
136
|
+
|
|
137
|
+
### Presets
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import {
|
|
141
|
+
conservativePolicy, // $0.10/call, $5/hr, $50/day
|
|
142
|
+
standardPolicy, // $1.00/call, $25/hr, $200/day
|
|
143
|
+
liberalPolicy, // $10/call, $100/hr, $1000/day
|
|
144
|
+
unlimitedPolicy, // no limits (audit only)
|
|
145
|
+
customPolicy, // override any defaults
|
|
146
|
+
} from "@x402sentinel/x402";
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Preset | Per Call | Per Hour | Per Day |
|
|
150
|
+
|--------|---------|---------|---------|
|
|
151
|
+
| `conservativePolicy()` | $0.10 | $5.00 | $50.00 |
|
|
152
|
+
| `standardPolicy()` | $1.00 | $25.00 | $200.00 |
|
|
153
|
+
| `liberalPolicy()` | $10.00 | $100.00 | $1,000.00 |
|
|
154
|
+
| `unlimitedPolicy()` | -- | -- | -- |
|
|
155
|
+
|
|
156
|
+
### Custom Policy
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const policy = customPolicy({
|
|
160
|
+
maxPerCall: "5.00",
|
|
161
|
+
maxPerHour: "50.00",
|
|
162
|
+
spikeThreshold: 5.0,
|
|
163
|
+
blockedEndpoints: ["https://*.competitors.com/*"],
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Error Handling
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { SentinelBudgetError } from "@x402sentinel/x402";
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await secureFetch("https://api.example.com/expensive");
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err instanceof SentinelBudgetError) {
|
|
176
|
+
console.log(err.message);
|
|
177
|
+
// "Budget exceeded: $2.50 spent of $5.00 hourly limit on agent-weather-001"
|
|
178
|
+
console.log(err.violation.type); // "hourly"
|
|
179
|
+
console.log(err.violation.limit); // "5.00"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Audit Records
|
|
187
|
+
|
|
188
|
+
Every payment produces an `AuditRecord` with:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
{
|
|
192
|
+
id: "a1b2c3d4e5f6g7h8", // deterministic hash
|
|
193
|
+
agent_id: "agent-weather-001",
|
|
194
|
+
team: "data-ops",
|
|
195
|
+
human_sponsor: "alice@company.com",
|
|
196
|
+
amount: "0.50", // human-readable USDC
|
|
197
|
+
amount_raw: "500000", // base units
|
|
198
|
+
asset: "USDC",
|
|
199
|
+
network: "eip155:8453",
|
|
200
|
+
tx_hash: "0xabc...",
|
|
201
|
+
endpoint: "https://api.weather.com/v1/current",
|
|
202
|
+
method: "GET",
|
|
203
|
+
status_code: 200,
|
|
204
|
+
response_time_ms: 150,
|
|
205
|
+
policy_evaluation: "allowed",
|
|
206
|
+
budget_remaining: "24.50",
|
|
207
|
+
created_at: 1700000000000,
|
|
208
|
+
tags: ["production", "weather"],
|
|
209
|
+
metadata: { cost_center: "ENG-2024" },
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Storage Backends
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { MemoryStorage, FileStorage, ApiStorage } from "@x402sentinel/x402";
|
|
217
|
+
|
|
218
|
+
// In-memory (default) — 10k records, FIFO eviction
|
|
219
|
+
const memory = new MemoryStorage(10_000);
|
|
220
|
+
|
|
221
|
+
// JSONL file — persistent, append-only
|
|
222
|
+
const file = new FileStorage(".valeo/audit.jsonl");
|
|
223
|
+
|
|
224
|
+
// Remote API — batched writes to api.valeo.money
|
|
225
|
+
const api = new ApiStorage({ apiKey: "val_..." });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Querying
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { AuditLogger } from "@x402sentinel/x402";
|
|
232
|
+
|
|
233
|
+
const logger = new AuditLogger({ storage: memory });
|
|
234
|
+
|
|
235
|
+
const records = await logger.query({
|
|
236
|
+
agentId: "agent-weather-001",
|
|
237
|
+
startTime: Date.now() - 86400000,
|
|
238
|
+
status: ["allowed", "flagged"],
|
|
239
|
+
limit: 100,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const summary = await logger.summarize({ team: "data-ops" });
|
|
243
|
+
console.log(summary.total_spend); // "$1,234.56"
|
|
244
|
+
|
|
245
|
+
const csv = await logger.exportCSV();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Dashboard
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { SentinelDashboard } from "@x402sentinel/x402/dashboard";
|
|
254
|
+
|
|
255
|
+
const dashboard = new SentinelDashboard({ storage: myStorage });
|
|
256
|
+
|
|
257
|
+
// Spend reports
|
|
258
|
+
const report = await dashboard.getSpend({
|
|
259
|
+
agentId: "bot-1",
|
|
260
|
+
range: "last_day",
|
|
261
|
+
});
|
|
262
|
+
console.log(report.totalSpend, report.count);
|
|
263
|
+
|
|
264
|
+
// Agent summaries
|
|
265
|
+
const agents = await dashboard.getAgents();
|
|
266
|
+
|
|
267
|
+
// Alerts (violations + anomalies)
|
|
268
|
+
const alerts = await dashboard.getAlerts();
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Dashboard queries run locally against your storage backend. No remote API required.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Integration Guide
|
|
276
|
+
|
|
277
|
+
Sentinel wraps **any** x402-compatible fetch. It works with:
|
|
278
|
+
|
|
279
|
+
- `@x402/fetch` + `x402Client` (current)
|
|
280
|
+
- `x402-fetch` + viem wallet (legacy)
|
|
281
|
+
- Any function with the `fetch` signature that handles x402 internally
|
|
282
|
+
|
|
283
|
+
The wrapper never touches the response body. It reads only headers (`PAYMENT-RESPONSE`, `PAYMENT-REQUIRED`) for audit data.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## API Reference
|
|
288
|
+
|
|
289
|
+
### `wrapWithSentinel(fetch, config): typeof fetch`
|
|
290
|
+
|
|
291
|
+
Wraps an x402 fetch function with Sentinel instrumentation. Returns a drop-in replacement.
|
|
292
|
+
|
|
293
|
+
### `BudgetManager`
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
class BudgetManager {
|
|
297
|
+
constructor(policy: BudgetPolicy);
|
|
298
|
+
evaluate(context: PaymentContext): BudgetEvaluation;
|
|
299
|
+
record(amount: string, endpoint: string): void;
|
|
300
|
+
getState(): BudgetState;
|
|
301
|
+
reset(scope: 'hourly' | 'daily' | 'total'): void;
|
|
302
|
+
serialize(): string;
|
|
303
|
+
static deserialize(data: string, policy: BudgetPolicy): BudgetManager;
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### `AuditLogger`
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
class AuditLogger {
|
|
311
|
+
constructor(config?: AuditConfig);
|
|
312
|
+
log(record): Promise<AuditRecord>;
|
|
313
|
+
logBlocked(context, violation): Promise<AuditRecord>;
|
|
314
|
+
query(query: AuditQuery): Promise<AuditRecord[]>;
|
|
315
|
+
summarize(query?): Promise<AuditSummary>;
|
|
316
|
+
exportCSV(query?): Promise<string>;
|
|
317
|
+
exportJSON(query?): Promise<string>;
|
|
318
|
+
flush(): Promise<void>;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Error Classes
|
|
323
|
+
|
|
324
|
+
| Class | When | Fatal? |
|
|
325
|
+
|-------|------|--------|
|
|
326
|
+
| `SentinelBudgetError` | Budget limit exceeded | Yes (blocks payment) |
|
|
327
|
+
| `SentinelAuditError` | Audit write failed | No (never blocks) |
|
|
328
|
+
| `SentinelConfigError` | Invalid configuration | Yes (at init) |
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## Full Stack Product
|
|
333
|
+
|
|
334
|
+
Sentinel is an SDK + Dashboard + API + Docs in a single monorepo:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
├── / SDK (@x402sentinel/x402)
|
|
338
|
+
├── app/ Next.js 15 application
|
|
339
|
+
│ ├── /login API key authentication
|
|
340
|
+
│ ├── /dashboard Real-time analytics dashboard
|
|
341
|
+
│ ├── /docs Full documentation site
|
|
342
|
+
│ └── /api/v1/* REST API (18 endpoints)
|
|
343
|
+
├── Dockerfile Production Docker image
|
|
344
|
+
└── docker-compose.yml One-command deployment
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Quick Deploy
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
docker compose up --build -d
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Development
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
pnpm install
|
|
357
|
+
make seed # Populate demo data (500 payments)
|
|
358
|
+
make dev # Start dashboard at localhost:3000
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Login with demo API key: `sk_sentinel_demo_000`
|
|
362
|
+
|
|
363
|
+
### Available Make Commands
|
|
364
|
+
|
|
365
|
+
| Command | Description |
|
|
366
|
+
|---------|-------------|
|
|
367
|
+
| `make dev` | Start Next.js dev server |
|
|
368
|
+
| `make build` | Build SDK + app |
|
|
369
|
+
| `make test` | Run SDK tests |
|
|
370
|
+
| `make seed` | Seed demo data |
|
|
371
|
+
| `make docker-up` | Build & start Docker |
|
|
372
|
+
| `make docker-down` | Stop Docker |
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Roadmap
|
|
377
|
+
|
|
378
|
+
- Multi-agent budget coordination (shared team budgets)
|
|
379
|
+
- Escrow and pre-commitment flows
|
|
380
|
+
- Treasury management integration
|
|
381
|
+
- On-chain agent identity verification
|
|
382
|
+
- Real-time dashboard at [app.valeo.money](https://app.valeo.money)
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/utils/money.ts
|
|
2
|
+
var USDC_DECIMALS = 6;
|
|
3
|
+
var USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
|
|
4
|
+
function parseUSDC(human) {
|
|
5
|
+
const trimmed = human.trim();
|
|
6
|
+
if (trimmed === "") {
|
|
7
|
+
throw new Error("Cannot parse empty string as USDC amount");
|
|
8
|
+
}
|
|
9
|
+
const negative = trimmed.startsWith("-");
|
|
10
|
+
if (negative) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Negative USDC amounts are not allowed: "${human}"`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
const dotIndex = trimmed.indexOf(".");
|
|
16
|
+
if (dotIndex === -1) {
|
|
17
|
+
return BigInt(trimmed) * USDC_SCALE;
|
|
18
|
+
}
|
|
19
|
+
const wholePart = trimmed.slice(0, dotIndex) || "0";
|
|
20
|
+
let fracPart = trimmed.slice(dotIndex + 1);
|
|
21
|
+
if (fracPart.length > USDC_DECIMALS) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`USDC amount "${human}" exceeds ${USDC_DECIMALS} decimal places`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
fracPart = fracPart.padEnd(USDC_DECIMALS, "0");
|
|
27
|
+
return BigInt(wholePart) * USDC_SCALE + BigInt(fracPart);
|
|
28
|
+
}
|
|
29
|
+
function formatUSDC(raw) {
|
|
30
|
+
const isNeg = raw < 0n;
|
|
31
|
+
const abs = isNeg ? -raw : raw;
|
|
32
|
+
const whole = abs / USDC_SCALE;
|
|
33
|
+
const frac = abs % USDC_SCALE;
|
|
34
|
+
const fracStr = frac.toString().padStart(USDC_DECIMALS, "0");
|
|
35
|
+
return `${isNeg ? "-" : ""}${whole}.${fracStr}`;
|
|
36
|
+
}
|
|
37
|
+
function formatUSDCHuman(raw) {
|
|
38
|
+
const full = formatUSDC(raw);
|
|
39
|
+
const dotIndex = full.indexOf(".");
|
|
40
|
+
if (dotIndex === -1) return full;
|
|
41
|
+
let end = full.length;
|
|
42
|
+
while (end > dotIndex + 3 && full[end - 1] === "0") {
|
|
43
|
+
end--;
|
|
44
|
+
}
|
|
45
|
+
return full.slice(0, end);
|
|
46
|
+
}
|
|
47
|
+
function addUSDC(a, b) {
|
|
48
|
+
return formatUSDC(parseUSDC(a) + parseUSDC(b));
|
|
49
|
+
}
|
|
50
|
+
function compareUSDC(a, b) {
|
|
51
|
+
const diff = parseUSDC(a) - parseUSDC(b);
|
|
52
|
+
if (diff < 0n) return -1;
|
|
53
|
+
if (diff > 0n) return 1;
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
function calculateAverage(amounts) {
|
|
57
|
+
if (amounts.length === 0) return formatUSDC(0n);
|
|
58
|
+
let sum = 0n;
|
|
59
|
+
for (const a of amounts) {
|
|
60
|
+
sum += parseUSDC(a);
|
|
61
|
+
}
|
|
62
|
+
return formatUSDC(sum / BigInt(amounts.length));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/utils/time.ts
|
|
66
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
67
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
68
|
+
function getHourStart(now) {
|
|
69
|
+
const ref = now ?? Date.now();
|
|
70
|
+
return ref - ref % HOUR_MS;
|
|
71
|
+
}
|
|
72
|
+
function getDayStart(now) {
|
|
73
|
+
const ref = now ?? Date.now();
|
|
74
|
+
return ref - ref % DAY_MS;
|
|
75
|
+
}
|
|
76
|
+
function resolveTimeRange(range, now) {
|
|
77
|
+
const ref = now ?? Date.now();
|
|
78
|
+
if (typeof range === "object") return range;
|
|
79
|
+
switch (range) {
|
|
80
|
+
case "last_hour":
|
|
81
|
+
return { start: ref - HOUR_MS, end: ref };
|
|
82
|
+
case "last_day":
|
|
83
|
+
return { start: ref - DAY_MS, end: ref };
|
|
84
|
+
case "last_week":
|
|
85
|
+
return { start: ref - 7 * DAY_MS, end: ref };
|
|
86
|
+
case "last_month":
|
|
87
|
+
return { start: ref - 30 * DAY_MS, end: ref };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { addUSDC, calculateAverage, compareUSDC, formatUSDC, formatUSDCHuman, getDayStart, getHourStart, parseUSDC, resolveTimeRange };
|
|
92
|
+
//# sourceMappingURL=chunk-25PZCEL2.js.map
|
|
93
|
+
//# sourceMappingURL=chunk-25PZCEL2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/money.ts","../src/utils/time.ts"],"names":[],"mappings":"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","file":"chunk-25PZCEL2.js","sourcesContent":["/** USDC has 6 decimal places: 1 USDC = 1_000_000 base units */\nexport const USDC_DECIMALS = 6;\nconst USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);\n\n/**\n * Parse a human-readable USDC string (e.g., \"1.50\") into base units as bigint.\n * Handles integers, decimals with up to 6 places, and leading/trailing zeros.\n */\nexport function parseUSDC(human: string): bigint {\n const trimmed = human.trim();\n if (trimmed === \"\") {\n throw new Error(\"Cannot parse empty string as USDC amount\");\n }\n\n const negative = trimmed.startsWith(\"-\");\n if (negative) {\n throw new Error(\n `Negative USDC amounts are not allowed: \"${human}\"`\n );\n }\n\n const dotIndex = trimmed.indexOf(\".\");\n if (dotIndex === -1) {\n return BigInt(trimmed) * USDC_SCALE;\n }\n\n const wholePart = trimmed.slice(0, dotIndex) || \"0\";\n let fracPart = trimmed.slice(dotIndex + 1);\n\n if (fracPart.length > USDC_DECIMALS) {\n throw new Error(\n `USDC amount \"${human}\" exceeds ${USDC_DECIMALS} decimal places`\n );\n }\n\n fracPart = fracPart.padEnd(USDC_DECIMALS, \"0\");\n return BigInt(wholePart) * USDC_SCALE + BigInt(fracPart);\n}\n\n/** Format base units to a full 6-decimal USDC string (e.g., 1500000n -> \"1.500000\") */\nexport function formatUSDC(raw: bigint): string {\n const isNeg = raw < 0n;\n const abs = isNeg ? -raw : raw;\n const whole = abs / USDC_SCALE;\n const frac = abs % USDC_SCALE;\n const fracStr = frac.toString().padStart(USDC_DECIMALS, \"0\");\n return `${isNeg ? \"-\" : \"\"}${whole}.${fracStr}`;\n}\n\n/** Format base units to a trimmed human-readable string (e.g., 1500000n -> \"1.50\") */\nexport function formatUSDCHuman(raw: bigint): string {\n const full = formatUSDC(raw);\n const dotIndex = full.indexOf(\".\");\n if (dotIndex === -1) return full;\n\n let end = full.length;\n while (end > dotIndex + 3 && full[end - 1] === \"0\") {\n end--;\n }\n return full.slice(0, end);\n}\n\n/** Add two human-readable USDC amounts, return human-readable result */\nexport function addUSDC(a: string, b: string): string {\n return formatUSDC(parseUSDC(a) + parseUSDC(b));\n}\n\n/** Subtract b from a (both human-readable USDC), return human-readable result */\nexport function subtractUSDC(a: string, b: string): string {\n return formatUSDC(parseUSDC(a) - parseUSDC(b));\n}\n\n/** Compare two human-readable USDC amounts. Returns -1, 0, or 1. */\nexport function compareUSDC(a: string, b: string): -1 | 0 | 1 {\n const diff = parseUSDC(a) - parseUSDC(b);\n if (diff < 0n) return -1;\n if (diff > 0n) return 1;\n return 0;\n}\n\n/** Check if spent exceeds limit (both human-readable USDC strings) */\nexport function isOverBudget(spent: string, limit: string): boolean {\n return parseUSDC(spent) > parseUSDC(limit);\n}\n\n/** Calculate the average of human-readable USDC amounts. Returns \"0.000000\" for empty array. */\nexport function calculateAverage(amounts: string[]): string {\n if (amounts.length === 0) return formatUSDC(0n);\n let sum = 0n;\n for (const a of amounts) {\n sum += parseUSDC(a);\n }\n return formatUSDC(sum / BigInt(amounts.length));\n}\n","const HOUR_MS = 60 * 60 * 1000;\nconst DAY_MS = 24 * HOUR_MS;\n\n/** Check if a timestamp falls within `windowMs` milliseconds of now */\nexport function isWithinWindow(timestamp: number, windowMs: number, now?: number): boolean {\n const ref = now ?? Date.now();\n return ref - timestamp < windowMs;\n}\n\n/** Get the start of the current hour as a unix ms timestamp */\nexport function getHourStart(now?: number): number {\n const ref = now ?? Date.now();\n return ref - (ref % HOUR_MS);\n}\n\n/** Get the start of the current day (UTC) as a unix ms timestamp */\nexport function getDayStart(now?: number): number {\n const ref = now ?? Date.now();\n return ref - (ref % DAY_MS);\n}\n\n/** Format a unix ms timestamp as an ISO 8601 string */\nexport function formatTimestamp(ts: number): string {\n return new Date(ts).toISOString();\n}\n\n/** Resolve a named time range to epoch ms boundaries */\nexport function resolveTimeRange(\n range: \"last_hour\" | \"last_day\" | \"last_week\" | \"last_month\" | { start: number; end: number },\n now?: number,\n): { start: number; end: number } {\n const ref = now ?? Date.now();\n if (typeof range === \"object\") return range;\n switch (range) {\n case \"last_hour\":\n return { start: ref - HOUR_MS, end: ref };\n case \"last_day\":\n return { start: ref - DAY_MS, end: ref };\n case \"last_week\":\n return { start: ref - 7 * DAY_MS, end: ref };\n case \"last_month\":\n return { start: ref - 30 * DAY_MS, end: ref };\n }\n}\n"]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/money.ts
|
|
4
|
+
var USDC_DECIMALS = 6;
|
|
5
|
+
var USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);
|
|
6
|
+
function parseUSDC(human) {
|
|
7
|
+
const trimmed = human.trim();
|
|
8
|
+
if (trimmed === "") {
|
|
9
|
+
throw new Error("Cannot parse empty string as USDC amount");
|
|
10
|
+
}
|
|
11
|
+
const negative = trimmed.startsWith("-");
|
|
12
|
+
if (negative) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Negative USDC amounts are not allowed: "${human}"`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const dotIndex = trimmed.indexOf(".");
|
|
18
|
+
if (dotIndex === -1) {
|
|
19
|
+
return BigInt(trimmed) * USDC_SCALE;
|
|
20
|
+
}
|
|
21
|
+
const wholePart = trimmed.slice(0, dotIndex) || "0";
|
|
22
|
+
let fracPart = trimmed.slice(dotIndex + 1);
|
|
23
|
+
if (fracPart.length > USDC_DECIMALS) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`USDC amount "${human}" exceeds ${USDC_DECIMALS} decimal places`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
fracPart = fracPart.padEnd(USDC_DECIMALS, "0");
|
|
29
|
+
return BigInt(wholePart) * USDC_SCALE + BigInt(fracPart);
|
|
30
|
+
}
|
|
31
|
+
function formatUSDC(raw) {
|
|
32
|
+
const isNeg = raw < 0n;
|
|
33
|
+
const abs = isNeg ? -raw : raw;
|
|
34
|
+
const whole = abs / USDC_SCALE;
|
|
35
|
+
const frac = abs % USDC_SCALE;
|
|
36
|
+
const fracStr = frac.toString().padStart(USDC_DECIMALS, "0");
|
|
37
|
+
return `${isNeg ? "-" : ""}${whole}.${fracStr}`;
|
|
38
|
+
}
|
|
39
|
+
function formatUSDCHuman(raw) {
|
|
40
|
+
const full = formatUSDC(raw);
|
|
41
|
+
const dotIndex = full.indexOf(".");
|
|
42
|
+
if (dotIndex === -1) return full;
|
|
43
|
+
let end = full.length;
|
|
44
|
+
while (end > dotIndex + 3 && full[end - 1] === "0") {
|
|
45
|
+
end--;
|
|
46
|
+
}
|
|
47
|
+
return full.slice(0, end);
|
|
48
|
+
}
|
|
49
|
+
function addUSDC(a, b) {
|
|
50
|
+
return formatUSDC(parseUSDC(a) + parseUSDC(b));
|
|
51
|
+
}
|
|
52
|
+
function compareUSDC(a, b) {
|
|
53
|
+
const diff = parseUSDC(a) - parseUSDC(b);
|
|
54
|
+
if (diff < 0n) return -1;
|
|
55
|
+
if (diff > 0n) return 1;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
function calculateAverage(amounts) {
|
|
59
|
+
if (amounts.length === 0) return formatUSDC(0n);
|
|
60
|
+
let sum = 0n;
|
|
61
|
+
for (const a of amounts) {
|
|
62
|
+
sum += parseUSDC(a);
|
|
63
|
+
}
|
|
64
|
+
return formatUSDC(sum / BigInt(amounts.length));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/utils/time.ts
|
|
68
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
69
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
70
|
+
function getHourStart(now) {
|
|
71
|
+
const ref = now ?? Date.now();
|
|
72
|
+
return ref - ref % HOUR_MS;
|
|
73
|
+
}
|
|
74
|
+
function getDayStart(now) {
|
|
75
|
+
const ref = now ?? Date.now();
|
|
76
|
+
return ref - ref % DAY_MS;
|
|
77
|
+
}
|
|
78
|
+
function resolveTimeRange(range, now) {
|
|
79
|
+
const ref = now ?? Date.now();
|
|
80
|
+
if (typeof range === "object") return range;
|
|
81
|
+
switch (range) {
|
|
82
|
+
case "last_hour":
|
|
83
|
+
return { start: ref - HOUR_MS, end: ref };
|
|
84
|
+
case "last_day":
|
|
85
|
+
return { start: ref - DAY_MS, end: ref };
|
|
86
|
+
case "last_week":
|
|
87
|
+
return { start: ref - 7 * DAY_MS, end: ref };
|
|
88
|
+
case "last_month":
|
|
89
|
+
return { start: ref - 30 * DAY_MS, end: ref };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exports.addUSDC = addUSDC;
|
|
94
|
+
exports.calculateAverage = calculateAverage;
|
|
95
|
+
exports.compareUSDC = compareUSDC;
|
|
96
|
+
exports.formatUSDC = formatUSDC;
|
|
97
|
+
exports.formatUSDCHuman = formatUSDCHuman;
|
|
98
|
+
exports.getDayStart = getDayStart;
|
|
99
|
+
exports.getHourStart = getHourStart;
|
|
100
|
+
exports.parseUSDC = parseUSDC;
|
|
101
|
+
exports.resolveTimeRange = resolveTimeRange;
|
|
102
|
+
//# sourceMappingURL=chunk-7H4FRU7K.cjs.map
|
|
103
|
+
//# sourceMappingURL=chunk-7H4FRU7K.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/money.ts","../src/utils/time.ts"],"names":[],"mappings":"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","file":"chunk-7H4FRU7K.cjs","sourcesContent":["/** USDC has 6 decimal places: 1 USDC = 1_000_000 base units */\nexport const USDC_DECIMALS = 6;\nconst USDC_SCALE = 10n ** BigInt(USDC_DECIMALS);\n\n/**\n * Parse a human-readable USDC string (e.g., \"1.50\") into base units as bigint.\n * Handles integers, decimals with up to 6 places, and leading/trailing zeros.\n */\nexport function parseUSDC(human: string): bigint {\n const trimmed = human.trim();\n if (trimmed === \"\") {\n throw new Error(\"Cannot parse empty string as USDC amount\");\n }\n\n const negative = trimmed.startsWith(\"-\");\n if (negative) {\n throw new Error(\n `Negative USDC amounts are not allowed: \"${human}\"`\n );\n }\n\n const dotIndex = trimmed.indexOf(\".\");\n if (dotIndex === -1) {\n return BigInt(trimmed) * USDC_SCALE;\n }\n\n const wholePart = trimmed.slice(0, dotIndex) || \"0\";\n let fracPart = trimmed.slice(dotIndex + 1);\n\n if (fracPart.length > USDC_DECIMALS) {\n throw new Error(\n `USDC amount \"${human}\" exceeds ${USDC_DECIMALS} decimal places`\n );\n }\n\n fracPart = fracPart.padEnd(USDC_DECIMALS, \"0\");\n return BigInt(wholePart) * USDC_SCALE + BigInt(fracPart);\n}\n\n/** Format base units to a full 6-decimal USDC string (e.g., 1500000n -> \"1.500000\") */\nexport function formatUSDC(raw: bigint): string {\n const isNeg = raw < 0n;\n const abs = isNeg ? -raw : raw;\n const whole = abs / USDC_SCALE;\n const frac = abs % USDC_SCALE;\n const fracStr = frac.toString().padStart(USDC_DECIMALS, \"0\");\n return `${isNeg ? \"-\" : \"\"}${whole}.${fracStr}`;\n}\n\n/** Format base units to a trimmed human-readable string (e.g., 1500000n -> \"1.50\") */\nexport function formatUSDCHuman(raw: bigint): string {\n const full = formatUSDC(raw);\n const dotIndex = full.indexOf(\".\");\n if (dotIndex === -1) return full;\n\n let end = full.length;\n while (end > dotIndex + 3 && full[end - 1] === \"0\") {\n end--;\n }\n return full.slice(0, end);\n}\n\n/** Add two human-readable USDC amounts, return human-readable result */\nexport function addUSDC(a: string, b: string): string {\n return formatUSDC(parseUSDC(a) + parseUSDC(b));\n}\n\n/** Subtract b from a (both human-readable USDC), return human-readable result */\nexport function subtractUSDC(a: string, b: string): string {\n return formatUSDC(parseUSDC(a) - parseUSDC(b));\n}\n\n/** Compare two human-readable USDC amounts. Returns -1, 0, or 1. */\nexport function compareUSDC(a: string, b: string): -1 | 0 | 1 {\n const diff = parseUSDC(a) - parseUSDC(b);\n if (diff < 0n) return -1;\n if (diff > 0n) return 1;\n return 0;\n}\n\n/** Check if spent exceeds limit (both human-readable USDC strings) */\nexport function isOverBudget(spent: string, limit: string): boolean {\n return parseUSDC(spent) > parseUSDC(limit);\n}\n\n/** Calculate the average of human-readable USDC amounts. Returns \"0.000000\" for empty array. */\nexport function calculateAverage(amounts: string[]): string {\n if (amounts.length === 0) return formatUSDC(0n);\n let sum = 0n;\n for (const a of amounts) {\n sum += parseUSDC(a);\n }\n return formatUSDC(sum / BigInt(amounts.length));\n}\n","const HOUR_MS = 60 * 60 * 1000;\nconst DAY_MS = 24 * HOUR_MS;\n\n/** Check if a timestamp falls within `windowMs` milliseconds of now */\nexport function isWithinWindow(timestamp: number, windowMs: number, now?: number): boolean {\n const ref = now ?? Date.now();\n return ref - timestamp < windowMs;\n}\n\n/** Get the start of the current hour as a unix ms timestamp */\nexport function getHourStart(now?: number): number {\n const ref = now ?? Date.now();\n return ref - (ref % HOUR_MS);\n}\n\n/** Get the start of the current day (UTC) as a unix ms timestamp */\nexport function getDayStart(now?: number): number {\n const ref = now ?? Date.now();\n return ref - (ref % DAY_MS);\n}\n\n/** Format a unix ms timestamp as an ISO 8601 string */\nexport function formatTimestamp(ts: number): string {\n return new Date(ts).toISOString();\n}\n\n/** Resolve a named time range to epoch ms boundaries */\nexport function resolveTimeRange(\n range: \"last_hour\" | \"last_day\" | \"last_week\" | \"last_month\" | { start: number; end: number },\n now?: number,\n): { start: number; end: number } {\n const ref = now ?? Date.now();\n if (typeof range === \"object\") return range;\n switch (range) {\n case \"last_hour\":\n return { start: ref - HOUR_MS, end: ref };\n case \"last_day\":\n return { start: ref - DAY_MS, end: ref };\n case \"last_week\":\n return { start: ref - 7 * DAY_MS, end: ref };\n case \"last_month\":\n return { start: ref - 30 * DAY_MS, end: ref };\n }\n}\n"]}
|