@tallyforagents/sdk 0.1.1 → 0.3.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/dist/index.js CHANGED
@@ -64,6 +64,22 @@ function makeError(payload, status) {
64
64
  }
65
65
 
66
66
  // src/client.ts
67
+ var warnedRotations = /* @__PURE__ */ new Set();
68
+ function warnRotationOnce(key, until) {
69
+ const id = `${key.slice(0, 12)}:${until}`;
70
+ if (warnedRotations.has(id)) return;
71
+ warnedRotations.add(id);
72
+ const remaining = (() => {
73
+ const ms = new Date(until).getTime() - Date.now();
74
+ if (ms <= 0) return "shortly";
75
+ const h = Math.floor(ms / 36e5);
76
+ const m = Math.floor(ms % 36e5 / 6e4);
77
+ return h > 0 ? `${h}h ${m}m` : `${m}m`;
78
+ })();
79
+ console.warn(
80
+ `[tally] API key is in its rotation grace window (ends in ~${remaining}, at ${until}). Switch to the rotated key before then to avoid 401s.`
81
+ );
82
+ }
67
83
  var TallyClient = class {
68
84
  apiKey;
69
85
  baseUrl;
@@ -98,6 +114,8 @@ var TallyClient = class {
98
114
  },
99
115
  body: body === void 0 ? void 0 : JSON.stringify(body)
100
116
  });
117
+ const graceUntil = res.headers.get("tally-rotation-grace-until");
118
+ if (graceUntil) warnRotationOnce(this.apiKey, graceUntil);
101
119
  if (!res.ok) {
102
120
  let payload = {};
103
121
  try {
@@ -149,6 +167,88 @@ var AgentsResource = class {
149
167
  );
150
168
  return agent;
151
169
  }
170
+ /**
171
+ * Soft-deletes an agent. The row stays in the DB so historical
172
+ * transactions keep their attribution; future list/get responses
173
+ * hide it.
174
+ *
175
+ * Throws `ConflictError` (HTTP 409, `code: "has_active_grants"`) if
176
+ * the agent has any active permissions — revoke them first.
177
+ *
178
+ * Re-creating an agent with the same `id` via `upsert()` is the
179
+ * supported way to bring a deleted agent back.
180
+ */
181
+ async delete(id) {
182
+ await this.client.request(
183
+ "DELETE",
184
+ `/v1/agents/${encodeURIComponent(id)}`
185
+ );
186
+ }
187
+ };
188
+
189
+ // src/pagination.ts
190
+ var AsyncResourcePage = class {
191
+ constructor(fetchPage) {
192
+ this.fetchPage = fetchPage;
193
+ }
194
+ fetchPage;
195
+ /**
196
+ * Async-iterates every item across all pages. Stops when the server
197
+ * returns `next_cursor: null`.
198
+ *
199
+ * ```ts
200
+ * for await (const p of tally.payments.list({ status: "confirmed" })) {
201
+ * console.log(p.id);
202
+ * }
203
+ * ```
204
+ */
205
+ async *[Symbol.asyncIterator]() {
206
+ let cursor = null;
207
+ while (true) {
208
+ const page = await this.fetchPage(cursor);
209
+ for (const item of page.data) yield item;
210
+ if (!page.next_cursor) break;
211
+ cursor = page.next_cursor;
212
+ }
213
+ }
214
+ /**
215
+ * Collects items into an array, optionally bounded. `toArray()` with
216
+ * no argument pulls every page; `toArray(20)` stops after the first
217
+ * 20 items (across whatever page boundaries that crosses).
218
+ *
219
+ * ```ts
220
+ * const recent = await tally.payments.list().toArray(20);
221
+ * ```
222
+ */
223
+ async toArray(maxItems) {
224
+ const out = [];
225
+ for await (const item of this) {
226
+ out.push(item);
227
+ if (maxItems !== void 0 && out.length >= maxItems) break;
228
+ }
229
+ return out;
230
+ }
231
+ /**
232
+ * Returns just the first page (no auto-pagination). Use when the
233
+ * cursor itself matters — e.g., persisting it across runs to resume
234
+ * iteration later.
235
+ *
236
+ * ```ts
237
+ * const { data, next_cursor } = await tally.payments.list().firstPage();
238
+ * // … store next_cursor; next run: tally.payments.list().pageAfter(saved_cursor)
239
+ * ```
240
+ */
241
+ async firstPage() {
242
+ return this.fetchPage(null);
243
+ }
244
+ /**
245
+ * Returns the page that follows the given cursor. Useful for resuming
246
+ * iteration from a previously-persisted cursor, or for manual paging
247
+ * when the auto-iterator's eager fetching isn't a fit.
248
+ */
249
+ async pageAfter(cursor) {
250
+ return this.fetchPage(cursor);
251
+ }
152
252
  };
153
253
 
154
254
  // src/resources/payments.ts
@@ -192,10 +292,126 @@ var PaymentsResource = class {
192
292
  );
193
293
  return payment;
194
294
  }
295
+ /**
296
+ * Lists payments in the API key's account + mode, auto-paginated.
297
+ * Returns an `AsyncResourcePage` you can `for await` over to iterate
298
+ * every payment, or call `.toArray(n)` for a bounded read.
299
+ *
300
+ * Pending outbound rows are lazily refreshed from the chain on read,
301
+ * matching the dashboard's behavior — polling `list({ status: "pending" })`
302
+ * is a valid way to wait for confirmation.
303
+ *
304
+ * ```ts
305
+ * for await (const p of tally.payments.list({ status: "confirmed" })) {
306
+ * console.log(p.id, p.amount_usdc, p.to);
307
+ * }
308
+ *
309
+ * // Or bounded:
310
+ * const last20 = await tally.payments.list({ direction: "outbound" }).toArray(20);
311
+ * ```
312
+ */
313
+ list(filters = {}) {
314
+ const { limit, status, direction, agent_id, wallet, q } = filters;
315
+ return new AsyncResourcePage(async (cursor) => {
316
+ const qs = new URLSearchParams();
317
+ if (cursor) qs.set("cursor", cursor);
318
+ if (limit !== void 0) qs.set("limit", String(limit));
319
+ if (status) qs.set("status", status);
320
+ if (direction) qs.set("direction", direction);
321
+ if (agent_id) qs.set("agent_id", agent_id);
322
+ if (wallet) qs.set("wallet", wallet);
323
+ if (q) qs.set("q", q);
324
+ const query = qs.toString();
325
+ const path = query ? `/v1/payments?${query}` : "/v1/payments";
326
+ return this.client.request(
327
+ "GET",
328
+ path
329
+ );
330
+ });
331
+ }
332
+ };
333
+
334
+ // src/resources/permissions.ts
335
+ var PermissionsResource = class {
336
+ constructor(client) {
337
+ this.client = client;
338
+ }
339
+ client;
340
+ /**
341
+ * Lists active grants in the API key's account + mode, auto-paginated.
342
+ * Returns an `AsyncResourcePage` you can `for await` over to iterate
343
+ * every permission, or call `.toArray(n)` for a bounded read.
344
+ *
345
+ * Each row includes the granted caps, today's usage, and the rolling
346
+ * remaining-daily-allowance, so agent code can preflight a payment
347
+ * without round-tripping the policy bounds.
348
+ *
349
+ * ```ts
350
+ * for await (const p of tally.permissions.list({ agent_id: "research-bot" })) {
351
+ * console.log(`${p.wallet_display_name}: $${p.remaining_today_usdc} left`);
352
+ * }
353
+ * ```
354
+ */
355
+ list(filters = {}) {
356
+ const { limit, agent_id } = filters;
357
+ return new AsyncResourcePage(async (cursor) => {
358
+ const qs = new URLSearchParams();
359
+ if (cursor) qs.set("cursor", cursor);
360
+ if (limit !== void 0) qs.set("limit", String(limit));
361
+ if (agent_id) qs.set("agent_id", agent_id);
362
+ const query = qs.toString();
363
+ const path = query ? `/v1/permissions?${query}` : "/v1/permissions";
364
+ return this.client.request("GET", path);
365
+ });
366
+ }
367
+ };
368
+
369
+ // src/resources/wallets.ts
370
+ var WalletsResource = class {
371
+ constructor(client) {
372
+ this.client = client;
373
+ }
374
+ client;
375
+ /**
376
+ * Lists every wallet in the API key's account + mode. Wallets are
377
+ * returned oldest-first (so the "Main Wallet" auto-provisioned at
378
+ * sign-in shows up first).
379
+ */
380
+ async list() {
381
+ const { wallets } = await this.client.request(
382
+ "GET",
383
+ "/v1/wallets"
384
+ );
385
+ return wallets;
386
+ }
387
+ /**
388
+ * Provisions a new Privy server wallet in the API key's account + mode.
389
+ * The wallet is owned (in Privy) by the oldest `owner`-role member of
390
+ * the account, so SDK-created wallets behave identically to dashboard-
391
+ * created ones: the same passkey approves any future signer changes.
392
+ */
393
+ async create(input) {
394
+ const { wallet } = await this.client.request(
395
+ "POST",
396
+ "/v1/wallets",
397
+ input
398
+ );
399
+ return wallet;
400
+ }
195
401
  };
196
402
  var SIGNATURE_VERSION = "v1";
197
403
  var DEFAULT_TOLERANCE_SECONDS = 300;
198
404
  var WebhooksResource = class {
405
+ /**
406
+ * The TallyClient is optional so legacy zero-arg construction
407
+ * (`new WebhooksResource()`) still works for non-class consumers who
408
+ * only use `verifySignature`. List / create / revoke require a
409
+ * client; calling them on a clientless instance throws.
410
+ */
411
+ constructor(client) {
412
+ this.client = client;
413
+ }
414
+ client;
199
415
  /**
200
416
  * Verify a `tally-signature` header against the request body. Use in
201
417
  * your webhook handler before processing the payload.
@@ -216,6 +432,58 @@ var WebhooksResource = class {
216
432
  verifySignature(input) {
217
433
  return verifySignature(input);
218
434
  }
435
+ requireClient() {
436
+ if (!this.client) {
437
+ throw new Error(
438
+ "WebhooksResource was constructed without a TallyClient. Use `new Tally({ apiKey })` to enable list/create/revoke."
439
+ );
440
+ }
441
+ return this.client;
442
+ }
443
+ /**
444
+ * Lists every webhook endpoint in the API key's account + mode.
445
+ * Revoked endpoints are included (with `revoked_at` set) for audit.
446
+ *
447
+ * ```ts
448
+ * for (const w of await tally.webhooks.list()) {
449
+ * console.log(`${w.url} → ${w.events.join(",")}`);
450
+ * }
451
+ * ```
452
+ */
453
+ async list() {
454
+ const { webhooks } = await this.requireClient().request("GET", "/v1/webhooks");
455
+ return webhooks;
456
+ }
457
+ /**
458
+ * Creates a new webhook endpoint. The signing secret is returned in
459
+ * `webhook.secret` **exactly once** — store it now; it's not
460
+ * recoverable later. If you lose it, revoke and recreate.
461
+ *
462
+ * ```ts
463
+ * const webhook = await tally.webhooks.create({
464
+ * url: "https://example.com/tally/events",
465
+ * events: ["payment.confirmed", "payment.failed"],
466
+ * });
467
+ * console.log(webhook.secret); // shown once
468
+ * ```
469
+ */
470
+ async create(input) {
471
+ const { webhook } = await this.requireClient().request("POST", "/v1/webhooks", input);
472
+ return webhook;
473
+ }
474
+ /**
475
+ * Revokes a webhook endpoint by id. No further deliveries will be
476
+ * enqueued; deliveries already in-flight are not cancelled.
477
+ * Idempotent: revoking an already-revoked endpoint is a no-op.
478
+ *
479
+ * ```ts
480
+ * await tally.webhooks.revoke("whk_01HXYZ...");
481
+ * ```
482
+ */
483
+ async revoke(id) {
484
+ const { webhook } = await this.requireClient().request("POST", `/v1/webhooks/${encodeURIComponent(id)}/revoke`);
485
+ return webhook;
486
+ }
219
487
  };
220
488
  function verifySignature(input) {
221
489
  const { body, header, secret } = input;
@@ -250,11 +518,195 @@ function verifySignature(input) {
250
518
  return { ok: true };
251
519
  }
252
520
 
521
+ // src/resources/x402.ts
522
+ var X402_HEADER = "x-payment";
523
+ var SUPPORTED_NETWORKS = /* @__PURE__ */ new Set(["base-sepolia", "base"]);
524
+ var X402Resource = class {
525
+ #payments;
526
+ constructor(client) {
527
+ this.#payments = new PaymentsResource(client);
528
+ }
529
+ /**
530
+ * Fetch a URL that may be paywalled by the x402 protocol. If the
531
+ * service returns 402, the SDK pays via Tally and retries
532
+ * automatically. The agent code sees one call.
533
+ *
534
+ * ```ts
535
+ * const { response, payment } = await tally.x402.fetch(
536
+ * "https://api.example.com/weather?city=Tokyo",
537
+ * { agent_id: "hermes", wallet: agent.wallets[0].address }
538
+ * );
539
+ *
540
+ * if (response.ok) {
541
+ * const data = await response.json();
542
+ * if (payment) console.log(`paid ${payment.amount_usdc} USDC — ${payment.tx_hash}`);
543
+ * }
544
+ * ```
545
+ *
546
+ * Throws `TallyError` for SDK-side problems (network unreachable,
547
+ * malformed 402 body, unsupported network, payment refused by
548
+ * Tally, etc.). Lets non-200 retry responses pass through as-is.
549
+ */
550
+ async fetch(url, input) {
551
+ const initial = await this.#doFetch(
552
+ url,
553
+ input,
554
+ /*paymentTxHash*/
555
+ null
556
+ );
557
+ if (initial.status !== 402) {
558
+ return { response: initial, payment: null };
559
+ }
560
+ let parsed;
561
+ try {
562
+ parsed = await initial.json();
563
+ } catch {
564
+ throw new TallyError({
565
+ type: "x402_protocol",
566
+ message: "Service returned 402 with a non-JSON body.",
567
+ status: 0
568
+ });
569
+ }
570
+ const terms = parsed.accepts?.[0];
571
+ if (!terms) {
572
+ throw new TallyError({
573
+ type: "x402_protocol",
574
+ message: "402 response had no `accepts[]` payment terms.",
575
+ status: 0,
576
+ details: { x402Version: parsed.x402Version, error: parsed.error }
577
+ });
578
+ }
579
+ if (!SUPPORTED_NETWORKS.has(terms.network)) {
580
+ throw new TallyError({
581
+ type: "x402_protocol",
582
+ message: `x402 service requested unsupported network: ${terms.network}. Tally supports base-sepolia (test) and base (live).`,
583
+ status: 0,
584
+ details: { network: terms.network }
585
+ });
586
+ }
587
+ const atomicAmount = (() => {
588
+ try {
589
+ return BigInt(terms.maxAmountRequired);
590
+ } catch {
591
+ throw new TallyError({
592
+ type: "x402_protocol",
593
+ message: `x402 service quoted an unparseable amount: ${terms.maxAmountRequired}`,
594
+ status: 0
595
+ });
596
+ }
597
+ })();
598
+ const decimalAmount = formatAtomicUSDC(atomicAmount);
599
+ if (input.max_amount_usdc !== void 0) {
600
+ const capAtomic = parseDecimalUSDC(input.max_amount_usdc);
601
+ if (atomicAmount > capAtomic) {
602
+ throw new TallyError({
603
+ type: "x402_amount_exceeds_cap",
604
+ message: `x402 service requested ${decimalAmount} USDC, which exceeds the caller-supplied max_amount_usdc of ${input.max_amount_usdc}.`,
605
+ status: 0,
606
+ details: { requested_usdc: decimalAmount, max_usdc: input.max_amount_usdc }
607
+ });
608
+ }
609
+ }
610
+ const memo = input.memo ?? defaultMemo(url);
611
+ const idempotencyKey = input.idempotency_key ?? defaultIdempotencyKey(url);
612
+ let payment;
613
+ try {
614
+ payment = await this.#payments.create({
615
+ agent_id: input.agent_id,
616
+ wallet: input.wallet,
617
+ to: terms.payTo,
618
+ amount_usdc: decimalAmount,
619
+ memo,
620
+ idempotency_key: idempotencyKey
621
+ });
622
+ } catch (e) {
623
+ throw e;
624
+ }
625
+ if (!payment.tx_hash) {
626
+ throw new TallyError({
627
+ type: "x402_payment_missing_tx_hash",
628
+ message: "Tally accepted the payment but returned no tx_hash.",
629
+ status: 0,
630
+ details: { payment_id: payment.id }
631
+ });
632
+ }
633
+ const retry = await this.#doFetch(url, input, payment.tx_hash);
634
+ return {
635
+ response: retry,
636
+ payment: {
637
+ id: payment.id,
638
+ tx_hash: payment.tx_hash,
639
+ amount_usdc: decimalAmount,
640
+ to: terms.payTo,
641
+ network: terms.network,
642
+ memo: payment.memo
643
+ }
644
+ };
645
+ }
646
+ async #doFetch(url, input, paymentTxHash) {
647
+ const headers = { ...input.headers ?? {} };
648
+ if (paymentTxHash) headers[X402_HEADER] = paymentTxHash;
649
+ const init = {
650
+ method: input.method ?? "GET",
651
+ headers,
652
+ body: input.body ?? void 0
653
+ };
654
+ if (input.timeout_ms !== void 0) {
655
+ const controller = new AbortController();
656
+ const timeoutId = setTimeout(() => controller.abort(), input.timeout_ms);
657
+ init.signal = controller.signal;
658
+ try {
659
+ return await fetch(url, init);
660
+ } finally {
661
+ clearTimeout(timeoutId);
662
+ }
663
+ }
664
+ return await fetch(url, init);
665
+ }
666
+ };
667
+ function defaultMemo(url) {
668
+ try {
669
+ const u = new URL(url);
670
+ return `x402:${u.host}${u.pathname}`.slice(0, 200);
671
+ } catch {
672
+ return "x402";
673
+ }
674
+ }
675
+ function defaultIdempotencyKey(url) {
676
+ try {
677
+ const u = new URL(url);
678
+ return `x402:${u.host}${u.pathname}:${Date.now()}`.slice(0, 64);
679
+ } catch {
680
+ return `x402:${Date.now()}`;
681
+ }
682
+ }
683
+ function formatAtomicUSDC(atomic) {
684
+ const whole = atomic / 1000000n;
685
+ const frac = atomic % 1000000n;
686
+ const fracStr = frac.toString().padStart(6, "0").replace(/0+$/, "");
687
+ return fracStr ? `${whole}.${fracStr}` : `${whole}`;
688
+ }
689
+ function parseDecimalUSDC(decimal) {
690
+ if (!/^\d+(\.\d{1,6})?$/.test(decimal.trim())) {
691
+ throw new TallyError({
692
+ type: "validation_failed",
693
+ message: `Invalid decimal USDC amount: "${decimal}". Expected up to 6 fractional digits.`,
694
+ status: 0
695
+ });
696
+ }
697
+ const [whole, frac = ""] = decimal.trim().split(".");
698
+ const fracPadded = (frac + "000000").slice(0, 6);
699
+ return BigInt(whole) * 1000000n + BigInt(fracPadded);
700
+ }
701
+
253
702
  // src/index.ts
254
703
  var Tally = class {
255
704
  agents;
256
705
  payments;
706
+ permissions;
707
+ wallets;
257
708
  webhooks;
709
+ x402;
258
710
  // Expose the underlying client for advanced use (custom retries, etc.).
259
711
  // Internal callers go through the resource classes instead.
260
712
  client;
@@ -262,10 +714,13 @@ var Tally = class {
262
714
  this.client = new TallyClient(opts);
263
715
  this.agents = new AgentsResource(this.client);
264
716
  this.payments = new PaymentsResource(this.client);
265
- this.webhooks = new WebhooksResource();
717
+ this.permissions = new PermissionsResource(this.client);
718
+ this.wallets = new WalletsResource(this.client);
719
+ this.webhooks = new WebhooksResource(this.client);
720
+ this.x402 = new X402Resource(this.client);
266
721
  }
267
722
  };
268
723
 
269
- export { AuthenticationError, ConflictError, NotFoundError, RateLimitError, Tally, TallyError, ValidationError, verifySignature };
724
+ export { AsyncResourcePage, AuthenticationError, ConflictError, NotFoundError, RateLimitError, Tally, TallyError, ValidationError, verifySignature };
270
725
  //# sourceMappingURL=index.js.map
271
726
  //# sourceMappingURL=index.js.map