@ottocode/ai-sdk 0.1.7 → 0.2.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/README.md CHANGED
@@ -4,6 +4,8 @@ A drop-in SDK for accessing AI models (OpenAI, Anthropic, Google, Moonshot, Mini
4
4
 
5
5
  All you need is a Solana wallet — the SDK handles authentication, payment negotiation, and provider routing automatically.
6
6
 
7
+ Normal API requests use bearer auth. The SDK signs a wallet nonce once to exchange for a short-lived Setu token, reuses that token across requests, and refreshes it automatically when needed.
8
+
7
9
  ## Install
8
10
 
9
11
  ```bash
@@ -32,6 +34,8 @@ console.log(text);
32
34
 
33
35
  The SDK auto-resolves which provider to use based on the model name. It returns ai-sdk compatible model instances that work directly with `generateText()`, `streamText()`, etc.
34
36
 
37
+ Under the hood, the first protected request exchanges wallet auth headers for a bearer token via `POST /v1/auth/wallet-token`. Subsequent requests reuse `Authorization: Bearer <token>` until refresh is needed.
38
+
35
39
  ## Provider Auto-Resolution
36
40
 
37
41
  Models are resolved to providers by prefix:
@@ -103,6 +107,8 @@ const setu = createSetu({
103
107
 
104
108
  Monitor and control the payment lifecycle:
105
109
 
110
+ Request authentication and payment signing are separate: bearer auth is used for normal Setu HTTP requests, while your wallet still signs the x402 payment transaction during topups.
111
+
106
112
  ```ts
107
113
  const setu = createSetu({
108
114
  auth: { privateKey: '...' },
@@ -287,6 +293,8 @@ setu.registry.mapModel('some-model', 'openai');
287
293
 
288
294
  Use the x402-aware fetch wrapper directly:
289
295
 
296
+ `setu.fetch()` uses bearer auth for normal requests and automatically refreshes the Setu access token on `401` once before retrying.
297
+
290
298
  ```ts
291
299
  const customFetch = setu.fetch();
292
300
 
@@ -306,6 +314,7 @@ import {
306
314
  getPublicKeyFromPrivate,
307
315
  addAnthropicCacheControl,
308
316
  createSetuFetch,
317
+ createWalletContext,
309
318
  } from '@ottocode/ai-sdk';
310
319
 
311
320
  // Get wallet address from private key
@@ -324,16 +333,21 @@ const setuFetch = createSetuFetch({
324
333
  });
325
334
  ```
326
335
 
336
+ `createWalletContext()` remains available for advanced usage. Its wallet headers are now intended for token exchange only; regular API traffic should go through `createSetu()`, `setu.fetch()`, `createSetuFetch()`, or `fetchBalance()` so bearer auth refresh is handled automatically.
337
+
327
338
  ## How It Works
328
339
 
329
340
  1. You call `setu.model('claude-sonnet-4-20250514')` — the SDK resolves this to Anthropic
330
341
  2. It creates an ai-sdk provider (`@ai-sdk/anthropic`) pointed at the Setu proxy
331
342
  3. A custom fetch wrapper intercepts all requests to:
332
- - Inject wallet auth headers (address, nonce, signature)
343
+ - Exchange signed wallet headers for a short-lived bearer token when needed
344
+ - Inject `Authorization: Bearer <token>` into normal API requests
333
345
  - Inject Anthropic cache control (if enabled)
346
+ - Handle `401` by refreshing the bearer token once and retrying
334
347
  - Handle 402 responses by signing USDC payments via x402
335
348
  - Sniff balance/cost info from SSE stream comments
336
- 4. The Setu proxy verifies the wallet, checks balance, forwards to the real provider, tracks usage
349
+ 4. During topups, the wallet still signs the x402 transaction, but the `/v1/topup` HTTP request itself uses bearer auth
350
+ 5. The Setu proxy verifies the wallet/token, checks balance, forwards to the real provider, and tracks usage
337
351
 
338
352
  ## Requirements
339
353
 
package/package.json CHANGED
@@ -1,29 +1,29 @@
1
1
  {
2
- "name": "@ottocode/ai-sdk",
3
- "version": "0.1.7",
4
- "type": "module",
5
- "exports": {
6
- ".": "./src/index.ts",
7
- "./providers": "./src/providers/index.ts",
8
- "./types": "./src/types.ts"
9
- },
10
- "files": [
11
- "src"
12
- ],
13
- "dependencies": {
14
- "@ai-sdk/anthropic": "^3.0.0",
15
- "@ai-sdk/google": "^3.0.0",
16
- "@ai-sdk/openai": "^3.0.0",
17
- "@ai-sdk/openai-compatible": "^2.0.0",
18
- "@solana/web3.js": "^1.98.0",
19
- "bs58": "^6.0.0",
20
- "tweetnacl": "^1.0.3",
21
- "x402": "^1.1.0"
22
- },
23
- "peerDependencies": {
24
- "ai": ">=6.0.0"
25
- },
26
- "devDependencies": {
27
- "typescript": "~5.9.3"
28
- }
2
+ "name": "@ottocode/ai-sdk",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./providers": "./src/providers/index.ts",
8
+ "./types": "./src/types.ts"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "dependencies": {
14
+ "@ai-sdk/anthropic": "^3.0.0",
15
+ "@ai-sdk/google": "^3.0.0",
16
+ "@ai-sdk/openai": "^3.0.0",
17
+ "@ai-sdk/openai-compatible": "^2.0.0",
18
+ "@solana/web3.js": "^1.98.0",
19
+ "bs58": "^6.0.0",
20
+ "tweetnacl": "^1.0.3",
21
+ "x402": "^1.1.0"
22
+ },
23
+ "peerDependencies": {
24
+ "ai": ">=6.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "~5.9.3"
28
+ }
29
29
  }
package/src/auth.ts CHANGED
@@ -5,7 +5,13 @@ import type { SetuAuth } from './types.ts';
5
5
 
6
6
  export interface WalletContext {
7
7
  walletAddress: string;
8
- buildHeaders: () => Promise<Record<string, string>> | Record<string, string>;
8
+ buildWalletAuthHeaders:
9
+ | (() => Promise<Record<string, string>>)
10
+ | (() => Record<string, string>);
11
+ /** @deprecated Use buildWalletAuthHeaders() instead. */
12
+ buildHeaders:
13
+ | (() => Promise<Record<string, string>>)
14
+ | (() => Record<string, string>);
9
15
  keypair?: Keypair;
10
16
  privateKeyBytes?: Uint8Array;
11
17
  signTransaction?: (transaction: Uint8Array) => Promise<Uint8Array>;
@@ -18,18 +24,20 @@ export function createWalletContext(auth: SetuAuth): WalletContext {
18
24
  signNonce: customSignNonce,
19
25
  signTransaction,
20
26
  } = auth.signer;
27
+ const buildWalletAuthHeaders = async () => {
28
+ const nonce = Date.now().toString();
29
+ const signature = await customSignNonce(nonce);
30
+ return {
31
+ 'x-wallet-address': walletAddress,
32
+ 'x-wallet-nonce': nonce,
33
+ 'x-wallet-signature': signature,
34
+ };
35
+ };
21
36
  return {
22
37
  walletAddress,
23
38
  signTransaction,
24
- buildHeaders: async () => {
25
- const nonce = Date.now().toString();
26
- const signature = await customSignNonce(nonce);
27
- return {
28
- 'x-wallet-address': walletAddress,
29
- 'x-wallet-nonce': nonce,
30
- 'x-wallet-signature': signature,
31
- };
32
- },
39
+ buildWalletAuthHeaders,
40
+ buildHeaders: buildWalletAuthHeaders,
33
41
  };
34
42
  }
35
43
 
@@ -40,11 +48,14 @@ export function createWalletContext(auth: SetuAuth): WalletContext {
40
48
  const privateKeyBytes = bs58.decode(auth.privateKey);
41
49
  const keypair = Keypair.fromSecretKey(privateKeyBytes);
42
50
  const walletAddress = keypair.publicKey.toBase58();
51
+ const buildWalletAuthHeaders = () =>
52
+ buildWalletHeaders(walletAddress, privateKeyBytes);
43
53
  return {
44
54
  keypair,
45
55
  walletAddress,
46
56
  privateKeyBytes,
47
- buildHeaders: () => buildWalletHeaders(walletAddress, privateKeyBytes),
57
+ buildWalletAuthHeaders,
58
+ buildHeaders: buildWalletAuthHeaders,
48
59
  };
49
60
  }
50
61
 
@@ -67,6 +78,8 @@ export function buildWalletHeaders(
67
78
  };
68
79
  }
69
80
 
81
+ export const buildWalletAuthHeaders = buildWalletHeaders;
82
+
70
83
  export function getPublicKeyFromPrivate(privateKey?: string): string | null {
71
84
  if (!privateKey) return null;
72
85
  try {
package/src/balance.ts CHANGED
@@ -3,6 +3,7 @@ import { Keypair } from '@solana/web3.js';
3
3
  import type { SetuAuth, BalanceResponse, WalletUsdcBalance } from './types.ts';
4
4
  import type { WalletContext } from './auth.ts';
5
5
  import { createWalletContext } from './auth.ts';
6
+ import { createAccessTokenManager } from './token.ts';
6
7
 
7
8
  const DEFAULT_BASE_URL = 'https://api.setu.ottocode.io';
8
9
  const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
@@ -17,8 +18,10 @@ function isWalletContext(input: unknown): input is WalletContext {
17
18
  return (
18
19
  typeof input === 'object' &&
19
20
  input !== null &&
20
- 'buildHeaders' in input &&
21
- typeof (input as WalletContext).buildHeaders === 'function'
21
+ (('buildWalletAuthHeaders' in input &&
22
+ typeof (input as WalletContext).buildWalletAuthHeaders === 'function') ||
23
+ ('buildHeaders' in input &&
24
+ typeof (input as WalletContext).buildHeaders === 'function'))
22
25
  );
23
26
  }
24
27
 
@@ -31,9 +34,19 @@ export async function fetchBalance(
31
34
  ? walletOrAuth
32
35
  : createWalletContext(walletOrAuth);
33
36
  const url = trimTrailingSlash(baseURL ?? DEFAULT_BASE_URL);
34
- const headers = await wallet.buildHeaders();
37
+ const tokenManager = createAccessTokenManager({ wallet, baseURL: url });
38
+ const requestBalance = async (forceRefresh = false) => {
39
+ const accessToken = await tokenManager.getToken(forceRefresh);
40
+ return fetch(`${url}/v1/balance`, {
41
+ headers: { authorization: `Bearer ${accessToken}` },
42
+ });
43
+ };
35
44
 
36
- const response = await fetch(`${url}/v1/balance`, { headers });
45
+ let response = await requestBalance();
46
+ if (response.status === 401) {
47
+ tokenManager.invalidate();
48
+ response = await requestBalance(true);
49
+ }
37
50
 
38
51
  if (!response.ok) return null;
39
52
 
@@ -45,9 +58,36 @@ export async function fetchBalance(
45
58
  request_count: number;
46
59
  created_at?: string;
47
60
  last_request?: string;
61
+ scope?: 'wallet' | 'account';
62
+ payg?: {
63
+ wallet_balance_usd: number;
64
+ account_balance_usd: number;
65
+ raw_pool_usd: number;
66
+ effective_spendable_usd: number;
67
+ };
68
+ limits?: {
69
+ enabled: boolean;
70
+ daily_limit_usd: number | null;
71
+ daily_spent_usd: number;
72
+ daily_remaining_usd: number | null;
73
+ monthly_limit_usd: number | null;
74
+ monthly_spent_usd: number;
75
+ monthly_remaining_usd: number | null;
76
+ cap_remaining_usd: number | null;
77
+ } | null;
78
+ subscription?: {
79
+ active: boolean;
80
+ tier_id?: string;
81
+ tier_name?: string;
82
+ credits_included?: number;
83
+ credits_used?: number;
84
+ credits_remaining?: number;
85
+ period_start?: string;
86
+ period_end?: string;
87
+ } | null;
48
88
  };
49
89
 
50
- return {
90
+ const result: BalanceResponse = {
51
91
  walletAddress: data.wallet_address,
52
92
  balance: data.balance_usd,
53
93
  totalSpent: data.total_spent,
@@ -55,7 +95,49 @@ export async function fetchBalance(
55
95
  requestCount: data.request_count,
56
96
  createdAt: data.created_at,
57
97
  lastRequest: data.last_request,
98
+ scope: data.scope,
58
99
  };
100
+
101
+ if (data.payg) {
102
+ result.payg = {
103
+ walletBalanceUsd: data.payg.wallet_balance_usd,
104
+ accountBalanceUsd: data.payg.account_balance_usd,
105
+ rawPoolUsd: data.payg.raw_pool_usd,
106
+ effectiveSpendableUsd: data.payg.effective_spendable_usd,
107
+ };
108
+ }
109
+
110
+ if (data.limits !== undefined) {
111
+ result.limits = data.limits
112
+ ? {
113
+ enabled: data.limits.enabled,
114
+ dailyLimitUsd: data.limits.daily_limit_usd,
115
+ dailySpentUsd: data.limits.daily_spent_usd,
116
+ dailyRemainingUsd: data.limits.daily_remaining_usd,
117
+ monthlyLimitUsd: data.limits.monthly_limit_usd,
118
+ monthlySpentUsd: data.limits.monthly_spent_usd,
119
+ monthlyRemainingUsd: data.limits.monthly_remaining_usd,
120
+ capRemainingUsd: data.limits.cap_remaining_usd,
121
+ }
122
+ : null;
123
+ }
124
+
125
+ if (data.subscription !== undefined) {
126
+ result.subscription = data.subscription
127
+ ? {
128
+ active: data.subscription.active,
129
+ tierId: data.subscription.tier_id,
130
+ tierName: data.subscription.tier_name,
131
+ creditsIncluded: data.subscription.credits_included,
132
+ creditsUsed: data.subscription.credits_used,
133
+ creditsRemaining: data.subscription.credits_remaining,
134
+ periodStart: data.subscription.period_start,
135
+ periodEnd: data.subscription.period_end,
136
+ }
137
+ : null;
138
+ }
139
+
140
+ return result;
59
141
  } catch {
60
142
  return null;
61
143
  }
package/src/catalog.ts CHANGED
@@ -1176,6 +1176,53 @@ export const setuCatalog: SetuCatalog = {
1176
1176
  cache_read: 0.17587499999999998,
1177
1177
  },
1178
1178
  },
1179
+ {
1180
+ id: 'gpt-5.4',
1181
+ name: 'GPT-5.4',
1182
+ owned_by: 'openai',
1183
+ context_length: 1050000,
1184
+ max_output: 128000,
1185
+ reasoning: true,
1186
+ tool_call: true,
1187
+ attachment: true,
1188
+ temperature: false,
1189
+ knowledge: '2025-08-31',
1190
+ release_date: '2026-03-05',
1191
+ last_updated: '2026-03-05',
1192
+ open_weights: false,
1193
+ modalities: {
1194
+ input: ['text', 'image'],
1195
+ output: ['text'],
1196
+ },
1197
+ pricing: {
1198
+ input: 2.5124999999999997,
1199
+ output: 15.075,
1200
+ cache_read: 0.25125,
1201
+ },
1202
+ },
1203
+ {
1204
+ id: 'gpt-5.4-pro',
1205
+ name: 'GPT-5.4 Pro',
1206
+ owned_by: 'openai',
1207
+ context_length: 1050000,
1208
+ max_output: 128000,
1209
+ reasoning: true,
1210
+ tool_call: true,
1211
+ attachment: true,
1212
+ temperature: false,
1213
+ knowledge: '2025-08-31',
1214
+ release_date: '2026-03-05',
1215
+ last_updated: '2026-03-05',
1216
+ open_weights: false,
1217
+ modalities: {
1218
+ input: ['text', 'image'],
1219
+ output: ['text'],
1220
+ },
1221
+ pricing: {
1222
+ input: 30.15,
1223
+ output: 180.89999999999998,
1224
+ },
1225
+ },
1179
1226
  {
1180
1227
  id: 'glm-4.7',
1181
1228
  name: 'GLM-4.7',
@@ -1248,5 +1295,5 @@ export const setuCatalog: SetuCatalog = {
1248
1295
  },
1249
1296
  ],
1250
1297
  providers: ['anthropic', 'google', 'minimax', 'moonshot', 'openai', 'zai'],
1251
- lastUpdated: '2026-02-19',
1298
+ lastUpdated: '2026-03-06',
1252
1299
  } as const satisfies SetuCatalog;
package/src/fetch.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  } from './types.ts';
9
9
  import { pickPaymentRequirement, handlePayment } from './payment.ts';
10
10
  import { addAnthropicCacheControl } from './cache.ts';
11
+ import { createAccessTokenManager } from './token.ts';
11
12
 
12
13
  const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
13
14
  const DEFAULT_MAX_ATTEMPTS = 3;
@@ -186,6 +187,11 @@ export function createSetuFetch(options: CreateSetuFetchOptions) {
186
187
  const topupApprovalMode = payment?.topupApprovalMode ?? 'auto';
187
188
  const autoPayThresholdUsd = payment?.autoPayThresholdUsd ?? 0;
188
189
  const baseFetch = customFetch ?? globalThis.fetch.bind(globalThis);
190
+ const tokenManager = createAccessTokenManager({
191
+ wallet,
192
+ baseURL,
193
+ fetch: baseFetch,
194
+ });
189
195
 
190
196
  return async (
191
197
  input: Parameters<typeof fetch>[0],
@@ -195,6 +201,13 @@ export function createSetuFetch(options: CreateSetuFetchOptions) {
195
201
 
196
202
  while (attempt < maxAttempts) {
197
203
  attempt++;
204
+ const performAuthenticatedRequest = async (forceRefresh = false) => {
205
+ const headers = new Headers(init?.headers);
206
+ const accessToken = await tokenManager.getToken(forceRefresh);
207
+ headers.set('authorization', `Bearer ${accessToken}`);
208
+ return baseFetch(input, { ...init, body, headers });
209
+ };
210
+
198
211
  let body = init?.body;
199
212
  if (body && typeof body === 'string') {
200
213
  try {
@@ -213,13 +226,11 @@ export function createSetuFetch(options: CreateSetuFetchOptions) {
213
226
  } catch {}
214
227
  }
215
228
 
216
- const headers = new Headers(init?.headers);
217
- const walletHeaders = await wallet.buildHeaders();
218
- headers.set('x-wallet-address', walletHeaders['x-wallet-address']);
219
- headers.set('x-wallet-nonce', walletHeaders['x-wallet-nonce']);
220
- headers.set('x-wallet-signature', walletHeaders['x-wallet-signature']);
221
-
222
- const response = await baseFetch(input, { ...init, body, headers });
229
+ let response = await performAuthenticatedRequest();
230
+ if (response.status === 401) {
231
+ tokenManager.invalidate();
232
+ response = await performAuthenticatedRequest(true);
233
+ }
223
234
 
224
235
  if (response.status !== 402) {
225
236
  return wrapResponseWithBalanceSniffing(response, callbacks);
@@ -292,6 +303,7 @@ export function createSetuFetch(options: CreateSetuFetchOptions) {
292
303
  rpcURL,
293
304
  baseURL,
294
305
  baseFetch,
306
+ tokenManager,
295
307
  maxAttempts: remainingPayments,
296
308
  callbacks,
297
309
  });
package/src/payment.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  PaymentCallbacks,
10
10
  FetchFunction,
11
11
  } from './types.ts';
12
+ import type { AccessTokenManager } from './token.ts';
12
13
  import {
13
14
  address,
14
15
  getTransactionEncoder,
@@ -138,6 +139,7 @@ export async function processSinglePayment(args: {
138
139
  rpcURL: string;
139
140
  baseURL: string;
140
141
  baseFetch: FetchFunction;
142
+ tokenManager: AccessTokenManager;
141
143
  callbacks: PaymentCallbacks;
142
144
  }): Promise<{ attempts: number; balance?: number | string }> {
143
145
  args.callbacks.onPaymentSigning?.();
@@ -156,15 +158,26 @@ export async function processSinglePayment(args: {
156
158
  throw new Error(`Setu: ${userMsg}`);
157
159
  }
158
160
 
159
- const walletHeaders = await args.wallet.buildHeaders();
160
- const response = await args.baseFetch(`${args.baseURL}/v1/topup`, {
161
- method: 'POST',
162
- headers: { 'Content-Type': 'application/json', ...walletHeaders },
163
- body: JSON.stringify({
164
- paymentPayload,
165
- paymentRequirement: args.requirement,
166
- }),
167
- });
161
+ const sendTopupRequest = async (forceRefresh = false) => {
162
+ const accessToken = await args.tokenManager.getToken(forceRefresh);
163
+ return args.baseFetch(`${args.baseURL}/v1/topup`, {
164
+ method: 'POST',
165
+ headers: {
166
+ 'Content-Type': 'application/json',
167
+ authorization: `Bearer ${accessToken}`,
168
+ },
169
+ body: JSON.stringify({
170
+ paymentPayload,
171
+ paymentRequirement: args.requirement,
172
+ }),
173
+ });
174
+ };
175
+
176
+ let response = await sendTopupRequest();
177
+ if (response.status === 401) {
178
+ args.tokenManager.invalidate();
179
+ response = await sendTopupRequest(true);
180
+ }
168
181
 
169
182
  const rawBody = await response.text().catch(() => '');
170
183
  if (!response.ok) {
@@ -210,6 +223,7 @@ export async function handlePayment(args: {
210
223
  rpcURL: string;
211
224
  baseURL: string;
212
225
  baseFetch: FetchFunction;
226
+ tokenManager: AccessTokenManager;
213
227
  maxAttempts: number;
214
228
  callbacks: PaymentCallbacks;
215
229
  }): Promise<{ attemptsUsed: number }> {
package/src/token.ts ADDED
@@ -0,0 +1,155 @@
1
+ import type { WalletContext } from './auth.ts';
2
+ import type { FetchFunction } from './types.ts';
3
+
4
+ const DEFAULT_TOKEN_REFRESH_SKEW_MS = 60_000;
5
+ const DEFAULT_TOKEN_TTL_MS = 5 * 60_000;
6
+
7
+ interface AccessTokenState {
8
+ token: string;
9
+ expiresAt: number;
10
+ }
11
+
12
+ export interface AccessTokenManager {
13
+ getToken(forceRefresh?: boolean): Promise<string>;
14
+ invalidate(): void;
15
+ }
16
+
17
+ interface CreateAccessTokenManagerOptions {
18
+ wallet: WalletContext;
19
+ baseURL: string;
20
+ fetch?: FetchFunction;
21
+ tokenRefreshSkewMs?: number;
22
+ }
23
+
24
+ interface WalletTokenResponse {
25
+ accessToken?: string;
26
+ access_token?: string;
27
+ token?: string;
28
+ expiresAt?: number | string;
29
+ expires_at?: number | string;
30
+ expiresIn?: number | string;
31
+ expires_in?: number | string;
32
+ }
33
+
34
+ function trimTrailingSlash(url: string) {
35
+ return url.endsWith('/') ? url.slice(0, -1) : url;
36
+ }
37
+
38
+ function parseNumber(value: unknown): number | null {
39
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
40
+ if (typeof value === 'string' && value.trim()) {
41
+ const parsed = Number(value);
42
+ if (Number.isFinite(parsed)) return parsed;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function parseJwtExpiry(token: string): number | null {
48
+ const parts = token.split('.');
49
+ if (parts.length < 2) return null;
50
+ try {
51
+ const base64 = parts[1]?.replace(/-/g, '+').replace(/_/g, '/');
52
+ if (!base64) return null;
53
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
54
+ const json = JSON.parse(atob(padded)) as {
55
+ exp?: unknown;
56
+ };
57
+ const exp = parseNumber(json.exp);
58
+ return exp != null ? exp * 1000 : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function resolveExpiresAt(payload: WalletTokenResponse, token: string): number {
65
+ const expiresAt = parseNumber(payload.expiresAt ?? payload.expires_at);
66
+ if (expiresAt != null) {
67
+ return expiresAt > 1_000_000_000_000 ? expiresAt : expiresAt * 1000;
68
+ }
69
+
70
+ const expiresIn = parseNumber(payload.expiresIn ?? payload.expires_in);
71
+ if (expiresIn != null) {
72
+ return Date.now() + expiresIn * 1000;
73
+ }
74
+
75
+ return parseJwtExpiry(token) ?? Date.now() + DEFAULT_TOKEN_TTL_MS;
76
+ }
77
+
78
+ async function exchangeWalletToken(
79
+ wallet: WalletContext,
80
+ baseURL: string,
81
+ baseFetch: FetchFunction,
82
+ ): Promise<AccessTokenState> {
83
+ const walletHeaders = await (
84
+ wallet.buildWalletAuthHeaders ?? wallet.buildHeaders
85
+ )();
86
+ const response = await baseFetch(
87
+ `${trimTrailingSlash(baseURL)}/v1/auth/wallet-token`,
88
+ {
89
+ method: 'POST',
90
+ headers: walletHeaders,
91
+ },
92
+ );
93
+
94
+ if (!response.ok) {
95
+ const body = await response.text().catch(() => '');
96
+ throw new Error(
97
+ `Setu: wallet token exchange failed (${response.status})${body ? `: ${body}` : ''}`,
98
+ );
99
+ }
100
+
101
+ const payload = (await response.json()) as WalletTokenResponse;
102
+ const token = payload.accessToken ?? payload.access_token ?? payload.token;
103
+ if (!token) {
104
+ throw new Error(
105
+ 'Setu: wallet token exchange response missing access token.',
106
+ );
107
+ }
108
+
109
+ return {
110
+ token,
111
+ expiresAt: resolveExpiresAt(payload, token),
112
+ };
113
+ }
114
+
115
+ export function createAccessTokenManager(
116
+ options: CreateAccessTokenManagerOptions,
117
+ ): AccessTokenManager {
118
+ const {
119
+ wallet,
120
+ baseURL,
121
+ fetch: customFetch,
122
+ tokenRefreshSkewMs = DEFAULT_TOKEN_REFRESH_SKEW_MS,
123
+ } = options;
124
+ const baseFetch = customFetch ?? globalThis.fetch.bind(globalThis);
125
+ let state: AccessTokenState | null = null;
126
+ let inFlight: Promise<string> | null = null;
127
+
128
+ const hasValidToken = () =>
129
+ state != null && Date.now() + tokenRefreshSkewMs < state.expiresAt;
130
+
131
+ const refresh = async () => {
132
+ const next = await exchangeWalletToken(wallet, baseURL, baseFetch);
133
+ state = next;
134
+ return next.token;
135
+ };
136
+
137
+ return {
138
+ async getToken(forceRefresh = false) {
139
+ if (!forceRefresh && hasValidToken() && state) {
140
+ return state.token;
141
+ }
142
+
143
+ if (!inFlight) {
144
+ inFlight = refresh().finally(() => {
145
+ inFlight = null;
146
+ });
147
+ }
148
+
149
+ return inFlight;
150
+ },
151
+ invalidate() {
152
+ state = null;
153
+ },
154
+ };
155
+ }
package/src/types.ts CHANGED
@@ -128,6 +128,33 @@ export interface BalanceResponse {
128
128
  requestCount: number;
129
129
  createdAt?: string;
130
130
  lastRequest?: string;
131
+ scope?: 'wallet' | 'account';
132
+ payg?: {
133
+ walletBalanceUsd: number;
134
+ accountBalanceUsd: number;
135
+ rawPoolUsd: number;
136
+ effectiveSpendableUsd: number;
137
+ };
138
+ limits?: {
139
+ enabled: boolean;
140
+ dailyLimitUsd: number | null;
141
+ dailySpentUsd: number;
142
+ dailyRemainingUsd: number | null;
143
+ monthlyLimitUsd: number | null;
144
+ monthlySpentUsd: number;
145
+ monthlyRemainingUsd: number | null;
146
+ capRemainingUsd: number | null;
147
+ } | null;
148
+ subscription?: {
149
+ active: boolean;
150
+ tierId?: string;
151
+ tierName?: string;
152
+ creditsIncluded?: number;
153
+ creditsUsed?: number;
154
+ creditsRemaining?: number;
155
+ periodStart?: string;
156
+ periodEnd?: string;
157
+ } | null;
131
158
  }
132
159
 
133
160
  export interface WalletUsdcBalance {