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