agentex-creator-sdk 1.0.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +195 -0
  4. package/dist/packages/contracts/src/deployer-investigation.d.ts +1015 -0
  5. package/dist/packages/contracts/src/deployer-investigation.js +101 -0
  6. package/dist/packages/contracts/src/index.d.ts +1701 -0
  7. package/dist/packages/contracts/src/index.js +380 -0
  8. package/dist/packages/contracts/src/indexed-activity.d.ts +684 -0
  9. package/dist/packages/contracts/src/indexed-activity.js +71 -0
  10. package/dist/packages/contracts/src/indexed-agents.d.ts +299 -0
  11. package/dist/packages/contracts/src/indexed-agents.js +131 -0
  12. package/dist/packages/contracts/src/inspection.d.ts +906 -0
  13. package/dist/packages/contracts/src/inspection.js +114 -0
  14. package/dist/packages/contracts/src/kinds.d.ts +5398 -0
  15. package/dist/packages/contracts/src/kinds.js +156 -0
  16. package/dist/packages/contracts/src/report-presentation.d.ts +346 -0
  17. package/dist/packages/contracts/src/report-presentation.js +120 -0
  18. package/dist/packages/contracts/src/solana-inspection.d.ts +451 -0
  19. package/dist/packages/contracts/src/solana-inspection.js +94 -0
  20. package/dist/packages/contracts/src/token-market.d.ts +193 -0
  21. package/dist/packages/contracts/src/token-market.js +335 -0
  22. package/dist/packages/contracts/src/wallet-analysis.d.ts +866 -0
  23. package/dist/packages/contracts/src/wallet-analysis.js +89 -0
  24. package/dist/packages/contracts/src/watchtower.d.ts +1141 -0
  25. package/dist/packages/contracts/src/watchtower.js +196 -0
  26. package/dist/packages/contracts/src/workflow.d.ts +1568 -0
  27. package/dist/packages/contracts/src/workflow.js +651 -0
  28. package/dist/packages/inspector/src/decode.d.ts +23 -0
  29. package/dist/packages/inspector/src/decode.js +150 -0
  30. package/dist/packages/inspector/src/scope.d.ts +87 -0
  31. package/dist/packages/inspector/src/scope.js +64 -0
  32. package/dist/packages/model/src/analysis.d.ts +149 -0
  33. package/dist/packages/model/src/analysis.js +387 -0
  34. package/dist/packages/model/src/pricing.d.ts +38 -0
  35. package/dist/packages/model/src/pricing.js +49 -0
  36. package/dist/packages/model/src/retry.d.ts +20 -0
  37. package/dist/packages/model/src/retry.js +31 -0
  38. package/dist/packages/model/src/schema.d.ts +10 -0
  39. package/dist/packages/model/src/schema.js +51 -0
  40. package/dist/packages/model/src/summary.d.ts +91 -0
  41. package/dist/packages/model/src/summary.js +177 -0
  42. package/dist/packages/model/src/types.d.ts +81 -0
  43. package/dist/packages/model/src/types.js +19 -0
  44. package/dist/packages/monitoring/src/delivery.d.ts +32 -0
  45. package/dist/packages/monitoring/src/delivery.js +53 -0
  46. package/dist/packages/providers/src/chain-transport.d.ts +42 -0
  47. package/dist/packages/providers/src/chain-transport.js +57 -0
  48. package/dist/packages/providers/src/coverage.d.ts +105 -0
  49. package/dist/packages/providers/src/coverage.js +260 -0
  50. package/dist/packages/providers/src/health.d.ts +273 -0
  51. package/dist/packages/providers/src/health.js +505 -0
  52. package/dist/packages/providers/src/keyed.d.ts +96 -0
  53. package/dist/packages/providers/src/keyed.js +240 -0
  54. package/dist/packages/providers/src/snapshot.d.ts +61 -0
  55. package/dist/packages/providers/src/snapshot.js +77 -0
  56. package/dist/packages/publication/src/fixtures.d.ts +45 -0
  57. package/dist/packages/publication/src/fixtures.js +350 -0
  58. package/dist/packages/research/src/index.d.ts +188 -0
  59. package/dist/packages/research/src/index.js +829 -0
  60. package/dist/packages/runtime/src/checkpoints.d.ts +65 -0
  61. package/dist/packages/runtime/src/checkpoints.js +214 -0
  62. package/dist/packages/runtime/src/policy.d.ts +57 -0
  63. package/dist/packages/runtime/src/policy.js +296 -0
  64. package/dist/packages/sdk/src/bin/agentex-buyer.d.ts +2 -0
  65. package/dist/packages/sdk/src/bin/agentex-buyer.js +4 -0
  66. package/dist/packages/sdk/src/bin/agentex.d.ts +2 -0
  67. package/dist/packages/sdk/src/bin/agentex.js +3 -0
  68. package/dist/packages/sdk/src/buyer-cli.d.ts +10 -0
  69. package/dist/packages/sdk/src/buyer-cli.js +210 -0
  70. package/dist/packages/sdk/src/buyer.d.ts +534 -0
  71. package/dist/packages/sdk/src/buyer.js +441 -0
  72. package/dist/packages/sdk/src/cli.d.ts +14 -0
  73. package/dist/packages/sdk/src/cli.js +149 -0
  74. package/dist/packages/sdk/src/errors.d.ts +31 -0
  75. package/dist/packages/sdk/src/errors.js +24 -0
  76. package/dist/packages/sdk/src/index.d.ts +236 -0
  77. package/dist/packages/sdk/src/index.js +150 -0
  78. package/dist/packages/sdk/src/local.d.ts +11 -0
  79. package/dist/packages/sdk/src/local.js +110 -0
  80. package/dist/packages/sdk/src/rails.d.ts +59 -0
  81. package/dist/packages/sdk/src/rails.js +96 -0
  82. package/dist/packages/sdk/src/report.d.ts +121 -0
  83. package/dist/packages/sdk/src/report.js +114 -0
  84. package/dist/packages/sdk/src/version.d.ts +2 -0
  85. package/dist/packages/sdk/src/version.js +2 -0
  86. package/dist/packages/watchtower/src/index.d.ts +150 -0
  87. package/dist/packages/watchtower/src/index.js +786 -0
  88. package/dist/packages/workflow/src/registry.d.ts +61 -0
  89. package/dist/packages/workflow/src/registry.js +76 -0
  90. package/examples/README.md +34 -0
  91. package/examples/cli-usage.sh +30 -0
  92. package/examples/fixtures/base-weth-input.json +4 -0
  93. package/examples/focused-researcher.json +127 -0
  94. package/examples/pay-with-eth-robinhood.mts +37 -0
  95. package/examples/pay-with-usdc.mts +41 -0
  96. package/examples/quickstart.mts +73 -0
  97. package/package.json +50 -0
@@ -0,0 +1,505 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { z } from 'zod';
4
+ import { canonicalJson } from '../../contracts/src/index.js';
5
+ export const ProviderHealthCategorySchema = z.enum(['provider-stale', 'chain-mismatch', 'snapshot-mismatch', 'provider-unavailable', 'rate-limited', 'timeout', 'history-unavailable', 'invalid-response', 'request-rejected', 'execution-result', 'cancelled']);
6
+ export class ProviderHealthError extends Error {
7
+ category;
8
+ constructor(category, message) {
9
+ super(message);
10
+ this.category = category;
11
+ this.name = 'ProviderHealthError';
12
+ }
13
+ }
14
+ /** The caller's `beforeAttempt` permit refused an attempt. It is never retried or failed over; `original` is the caller's error. */
15
+ export class AttemptRefusedError extends ProviderHealthError {
16
+ original;
17
+ constructor(original) {
18
+ super('cancelled', 'The runtime refused a provider attempt.');
19
+ this.original = original;
20
+ this.name = 'AttemptRefusedError';
21
+ }
22
+ }
23
+ const RETRYABLE = new Set(['provider-stale', 'provider-unavailable', 'rate-limited', 'timeout', 'history-unavailable']);
24
+ function deepFreeze(value) { if (value && typeof value === 'object') {
25
+ Object.values(value).forEach(deepFreeze);
26
+ Object.freeze(value);
27
+ } return value; }
28
+ /** Thresholds leave wide margin over each chain's block interval so a merely slow endpoint is not excluded; only a head that stopped advancing is. */
29
+ export const HEAD_FRESHNESS_POLICIES = deepFreeze({
30
+ ethereum: { family: 'evm', identity: '0x1', headTag: 'latest', maximumHeadAgeSeconds: 180, observedTypicalHeadAge: '12 s slots; a healthy latest block is under about 30 s old', observedAt: '2026-09-15' },
31
+ base: { family: 'evm', identity: '0x2105', headTag: 'latest', maximumHeadAgeSeconds: 120, observedTypicalHeadAge: '2 s blocks; a healthy latest block is seconds old', observedAt: '2026-09-15' },
32
+ robinhood: { family: 'evm', identity: '0x1237', headTag: 'latest', maximumHeadAgeSeconds: 300, observedTypicalHeadAge: 'latest was seconds old on the official endpoint (observed 2026-09-14)', observedAt: '2026-09-14' },
33
+ 'robinhood-testnet': { family: 'evm', identity: '0xb626', headTag: 'latest', maximumHeadAgeSeconds: 300, observedTypicalHeadAge: 'latest was seconds old on the official endpoint (observed 2026-09-14)', observedAt: '2026-09-14' },
34
+ solana: { family: 'solana', identity: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d', headTag: 'confirmed', maximumHeadAgeSeconds: 120, observedTypicalHeadAge: 'about 0.4 s slots; a healthy confirmed slot is seconds old', observedAt: '2026-09-15' },
35
+ 'solana-devnet': { family: 'solana', identity: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG', headTag: 'confirmed', maximumHeadAgeSeconds: 120, observedTypicalHeadAge: 'about 0.4 s slots; a healthy confirmed slot is seconds old', observedAt: '2026-09-15' },
36
+ });
37
+ export const endpointHost = (url) => { try {
38
+ const parsed = new URL(url);
39
+ return parsed.host || 'invalid-endpoint';
40
+ }
41
+ catch {
42
+ return 'invalid-endpoint';
43
+ } };
44
+ export function categorizeProviderError(error, signal) {
45
+ if (error instanceof ProviderHealthError)
46
+ return error.category;
47
+ if (signal?.aborted)
48
+ return signal.reason?.name === 'TimeoutError' ? 'timeout' : 'cancelled';
49
+ const parts = [];
50
+ let current = error;
51
+ let rejected = false;
52
+ for (let index = 0; current && typeof current === 'object' && index < 8; index++) {
53
+ const item = current;
54
+ if (item.status === 429 || item.code === -32005 || item.category === 'rate-limited')
55
+ return 'rate-limited';
56
+ // A contract revert is a definitive answer from a healthy endpoint: never retried, failed over or excluded. Keyed transports
57
+ // (keyed.ts KeyedProviderError) carry the JSON-RPC code as `rpcCode` and their own classification as `category`.
58
+ if (item.code === 3 || item.rpcCode === 3 || item.category === 'execution-result')
59
+ return 'execution-result';
60
+ if (item.code === -32602 || item.code === -32601)
61
+ rejected = true;
62
+ // A response over the run's byte cap is too large for every endpoint (they share the cap): failing over only repeats it. The caller
63
+ // receives the original error, which research tools read as `limit-exceeded` and answer with a narrower request.
64
+ if (item.category === 'limit-exceeded')
65
+ return 'request-rejected';
66
+ for (const part of [item.name, item.message, item.details])
67
+ if (typeof part === 'string')
68
+ parts.push(part);
69
+ current = item.cause;
70
+ }
71
+ const text = parts.join(' ').toLowerCase();
72
+ if (/responsebodysize|response body.*size limit|exceeds the byte limit/.test(text))
73
+ return 'request-rejected';
74
+ if (/429|rate limit|too many requests/.test(text))
75
+ return 'rate-limited';
76
+ if (/missing trie|historical state|state.*prun|archive|metadata is not found|header not found|minimum ledger|not available/.test(text))
77
+ return 'history-unavailable';
78
+ if (/execution reverted|revert|invalid opcode|out of gas|insufficient funds/.test(text))
79
+ return 'execution-result';
80
+ if (/timeout|timed out/.test(text))
81
+ return 'timeout';
82
+ // Load-balanced public endpoints answer policy refusals and backend lag with an invalid-params code; another endpoint can serve the same request.
83
+ if (/request blocked|beyond (the )?current head/.test(text))
84
+ return 'provider-unavailable';
85
+ if (rejected)
86
+ return 'request-rejected';
87
+ return 'provider-unavailable';
88
+ }
89
+ const METHOD_UNITS = { eth_getLogs: 5, eth_estimateGas: 2, getTransaction: 2, getSignaturesForAddress: 3, simulateTransaction: 5, getMultipleAccounts: 2, getBlock: 5 };
90
+ /** Relative request weight for budgeting. It is not a price: every free public source remains unpriced. */
91
+ export const methodCostUnits = (method) => METHOD_UNITS[method] ?? 1;
92
+ export class ProviderTelemetry {
93
+ #stats = new Map();
94
+ #cache = { hits: 0, misses: 0, stores: 0, rejectedPrivate: 0 };
95
+ #entry(endpoint) {
96
+ const key = `${endpoint.chain}:${endpoint.id}`;
97
+ let entry = this.#stats.get(key);
98
+ if (!entry) {
99
+ if (this.#stats.size >= 64)
100
+ throw new ProviderHealthError('invalid-response', 'Telemetry endpoint limit reached.');
101
+ entry = { id: endpoint.id, chain: endpoint.chain, host: endpoint.host, status: 'unobserved', lastCategory: null, lastObservedAt: null, requests: 0, successes: 0, failures: {}, retries: 0, failovers: 0, identityChecks: 0, crossChecks: 0, crossCheckMismatches: 0, cacheHits: 0, costUnits: 0, latencyTotalMs: 0, latencyMaxMs: 0, lastLatencyMs: null, freshness: null };
102
+ this.#stats.set(key, entry);
103
+ }
104
+ return entry;
105
+ }
106
+ attempt(endpoint, event) {
107
+ const entry = this.#entry(endpoint);
108
+ const latency = Math.max(0, Math.round(event.latencyMs));
109
+ entry.requests++;
110
+ entry.costUnits += methodCostUnits(event.method);
111
+ entry.latencyTotalMs += latency;
112
+ entry.latencyMaxMs = Math.max(entry.latencyMaxMs, latency);
113
+ entry.lastLatencyMs = latency;
114
+ entry.lastObservedAt = (event.at ?? new Date()).toISOString();
115
+ if (event.retry)
116
+ entry.retries++;
117
+ if (event.purpose === 'identity')
118
+ entry.identityChecks++;
119
+ if (event.purpose === 'cross-check')
120
+ entry.crossChecks++;
121
+ if (event.outcome === 'success' || event.outcome === 'execution-result') {
122
+ entry.successes++;
123
+ if (entry.status !== 'excluded' && entry.status !== 'provider-stale')
124
+ entry.status = 'healthy';
125
+ }
126
+ else {
127
+ entry.failures[event.outcome] = (entry.failures[event.outcome] ?? 0) + 1;
128
+ entry.lastCategory = event.outcome;
129
+ if (entry.status !== 'excluded' && entry.status !== 'provider-stale')
130
+ entry.status = 'degraded';
131
+ }
132
+ }
133
+ mark(endpoint, status, category) { const entry = this.#entry(endpoint); entry.status = status; if (category)
134
+ entry.lastCategory = category; }
135
+ failover(endpoint) { this.#entry(endpoint).failovers++; }
136
+ crossCheckMismatch(endpoint) { this.#entry(endpoint).crossCheckMismatches++; }
137
+ cacheHit(endpoint) { this.#cache.hits++; if (endpoint)
138
+ this.#entry(endpoint).cacheHits++; }
139
+ cacheMiss() { this.#cache.misses++; }
140
+ cacheStore() { this.#cache.stores++; }
141
+ cachePrivateRefusal() { this.#cache.rejectedPrivate++; }
142
+ freshness(endpoint, observation) {
143
+ const { stale, ...freshness } = observation;
144
+ const entry = this.#entry(endpoint);
145
+ entry.freshness = freshness;
146
+ if (stale) {
147
+ entry.status = 'provider-stale';
148
+ entry.lastCategory = 'provider-stale';
149
+ }
150
+ else if (entry.status === 'provider-stale' || entry.status === 'unobserved')
151
+ entry.status = 'healthy';
152
+ }
153
+ snapshot(now = new Date()) {
154
+ return { schemaVersion: 'agentex.provider-health.v1', generatedAt: now.toISOString(), costStatus: 'unpriced',
155
+ note: 'Cost units are relative request weights for budgeting, not invoices. Free public endpoints carry no SLA, archive guarantee or rate commitment.',
156
+ cache: { scope: 'public-chain-data-only', ...this.#cache },
157
+ endpoints: [...this.#stats.values()].sort((a, b) => `${a.chain}:${a.id}` < `${b.chain}:${b.id}` ? -1 : 1).map((entry) => ({ ...structuredClone(entry), meanLatencyMs: entry.requests ? Math.round(entry.latencyTotalMs / entry.requests) : null })) };
158
+ }
159
+ reset() { this.#stats.clear(); Object.assign(this.#cache, { hits: 0, misses: 0, stores: 0, rejectedPrivate: 0 }); }
160
+ }
161
+ /** Process-wide operator telemetry. It holds hosts, counters and freshness only: no request parameters, results or tenant identifiers. */
162
+ export const PROVIDER_TELEMETRY = new ProviderTelemetry();
163
+ const digest = (value) => createHash('sha256').update(canonicalJson(JSON.parse(JSON.stringify(value, (_key, item) => typeof item === 'bigint' ? item.toString() : item)))).digest('hex');
164
+ const numericTag = (tag) => typeof tag === 'string' && /^0x(?:0|[1-9a-fA-F][0-9a-fA-F]{0,15})$/.test(tag) ? BigInt(tag) : null;
165
+ const PRIVATE_EVM_METHODS = new Set(['eth_estimateGas', 'eth_sendRawTransaction', 'eth_sendTransaction', 'eth_sign', 'eth_signTransaction', 'eth_accounts']);
166
+ const PRIVATE_SOLANA_METHODS = new Set(['simulateTransaction', 'sendTransaction', 'requestAirdrop']);
167
+ /** The block number a state request is pinned to, or null for moving tags and unpinned requests. */
168
+ export function referencedBlockNumber(method, params) {
169
+ if (['eth_getCode', 'eth_getBalance', 'eth_getTransactionCount', 'eth_call', 'eth_estimateGas'].includes(method))
170
+ return numericTag(params.at(-1));
171
+ if (method === 'eth_getStorageAt')
172
+ return numericTag(params[2]);
173
+ if (method === 'eth_getLogs') {
174
+ const filter = params[0];
175
+ return numericTag(filter?.toBlock);
176
+ }
177
+ return null;
178
+ }
179
+ /**
180
+ * Shared cache key for public chain data only. Reads are not final, so a key always binds the exact block hash this transport observed
181
+ * at the referenced height: a reorg changes the hash and therefore the key. Block headers are never cached (they are the reorg check),
182
+ * Solana data is never cached (confirmed data can still roll back), and proposed payloads, sender-context calls, moving tags and unpinned
183
+ * reads are never cacheable, so one tenant's private request cannot be served to another.
184
+ */
185
+ export function publicCacheKey(context, method, params) {
186
+ if (context.family !== 'evm' || PRIVATE_EVM_METHODS.has(method) || method === 'eth_getBlockByNumber')
187
+ return null;
188
+ if (method === 'eth_call') {
189
+ const call = params[0];
190
+ if (params.length !== 2 || !call || typeof call !== 'object' || Object.keys(call).some((key) => !['to', 'data', 'gas'].includes(key)))
191
+ return null;
192
+ }
193
+ else if (method === 'eth_getLogs') {
194
+ const filter = params[0];
195
+ if (params.length !== 1 || !filter || numericTag(filter.fromBlock) === null || 'blockHash' in filter)
196
+ return null;
197
+ }
198
+ else if (!['eth_getCode', 'eth_getStorageAt', 'eth_getBalance', 'eth_getTransactionCount'].includes(method))
199
+ return null;
200
+ const number = referencedBlockNumber(method, params);
201
+ const anchor = number === null ? undefined : context.anchors.get(number.toString());
202
+ return anchor ? `evm:${context.identity}:block:${number}:${anchor.hash}:${digest({ method, params })}` : null;
203
+ }
204
+ export class PublicDataCache {
205
+ maximumEntries;
206
+ maximumBytes;
207
+ maximumEntryBytes;
208
+ #entries = new Map();
209
+ #bytes = 0;
210
+ constructor(maximumEntries = 2000, maximumBytes = 32 * 1024 * 1024, maximumEntryBytes = 262144) {
211
+ this.maximumEntries = maximumEntries;
212
+ this.maximumBytes = maximumBytes;
213
+ this.maximumEntryBytes = maximumEntryBytes;
214
+ }
215
+ get(key) { const entry = this.#entries.get(key); if (!entry)
216
+ return undefined; this.#entries.delete(key); this.#entries.set(key, entry); return structuredClone(entry.value); }
217
+ set(key, value) {
218
+ const bytes = Buffer.byteLength(JSON.stringify(value ?? null, (_key, item) => typeof item === 'bigint' ? item.toString() : item));
219
+ if (bytes > this.maximumEntryBytes)
220
+ return false;
221
+ const prior = this.#entries.get(key);
222
+ if (prior) {
223
+ this.#bytes -= prior.bytes;
224
+ this.#entries.delete(key);
225
+ }
226
+ this.#entries.set(key, { value: structuredClone(value), bytes });
227
+ this.#bytes += bytes;
228
+ while (this.#entries.size > this.maximumEntries || this.#bytes > this.maximumBytes) {
229
+ const oldest = this.#entries.keys().next().value;
230
+ this.#bytes -= this.#entries.get(oldest).bytes;
231
+ this.#entries.delete(oldest);
232
+ }
233
+ return true;
234
+ }
235
+ get size() { return this.#entries.size; }
236
+ clear() { this.#entries.clear(); this.#bytes = 0; }
237
+ }
238
+ export const PUBLIC_DATA_CACHE = new PublicDataCache();
239
+ export function createResilientTransport(options) {
240
+ const registered = HEAD_FRESHNESS_POLICIES[options.chain];
241
+ if (!registered)
242
+ throw new ProviderHealthError('invalid-response', `No head freshness policy is registered for ${options.chain}.`);
243
+ const policy = registered;
244
+ const endpoints = z.array(z.object({ id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,31}$/), url: z.string() })).min(1).max(4).parse(options.endpoints.map(({ id, url }) => ({ id, url })));
245
+ if (new Set(endpoints.map((item) => item.id)).size !== endpoints.length)
246
+ throw new ProviderHealthError('invalid-response', 'Endpoint ids must be unique.');
247
+ const maximumAttempts = z.number().int().min(1).max(3).parse(options.maximumAttempts ?? (endpoints.length > 1 ? 2 : 1));
248
+ let retryBudget = z.number().int().min(0).max(16).parse(options.retryBudget ?? (endpoints.length > 1 ? 4 : 0));
249
+ const telemetry = options.telemetry ?? PROVIDER_TELEMETRY;
250
+ const now = options.now ?? (() => new Date());
251
+ const categorize = options.categorize ?? categorizeProviderError;
252
+ const cache = options.cache === undefined ? PUBLIC_DATA_CACHE : options.cache;
253
+ const state = options.endpoints.map((endpoint, index) => ({ ...endpoint, index, chain: options.chain, host: endpointHost(endpoint.url), excluded: null, identity: 'unverified', freshnessChecked: false }));
254
+ const anchors = new Map();
255
+ let active = 0;
256
+ const identityMethod = policy.family === 'evm' ? 'eth_chainId' : 'getGenesisHash';
257
+ const available = (item) => !item.excluded || (item.excluded.until !== null && item.excluded.until <= now().getTime());
258
+ const exclude = (item, category, cooldownMs) => { item.excluded = { category, until: cooldownMs === null ? null : now().getTime() + cooldownMs }; telemetry.mark(item, category === 'provider-stale' ? 'provider-stale' : 'excluded', category); };
259
+ const spend = () => { if (retryBudget <= 0)
260
+ return false; retryBudget--; return true; };
261
+ const crossChecked = new Set();
262
+ let attemptCounter = 0;
263
+ let lastServed = null;
264
+ const cachedFrom = new Map();
265
+ async function send(item, method, params, signal, purpose, retry) {
266
+ let complete;
267
+ try {
268
+ complete = await options.beforeAttempt?.({ endpointId: item.id, method, purpose, attempt: ++attemptCounter });
269
+ }
270
+ catch (error) {
271
+ throw new AttemptRefusedError(error);
272
+ }
273
+ // A runtime refusal while settling a hidden permit (for example a lost lease) is never retried or failed over.
274
+ const settle = async (outcome) => { if (!complete)
275
+ return; try {
276
+ await complete({ outcome });
277
+ }
278
+ catch (error) {
279
+ throw new AttemptRefusedError(error);
280
+ } };
281
+ const started = performance.now();
282
+ let result;
283
+ try {
284
+ result = await item.transport.request(method, params, signal);
285
+ }
286
+ catch (error) {
287
+ const outcome = categorize(error, signal);
288
+ telemetry.attempt(item, { method, outcome, latencyMs: performance.now() - started, purpose, retry, at: now() });
289
+ await settle(outcome);
290
+ throw error;
291
+ }
292
+ telemetry.attempt(item, { method, outcome: 'success', latencyMs: performance.now() - started, purpose, retry, at: now() });
293
+ await settle('success');
294
+ return result;
295
+ }
296
+ const identityOf = (value) => policy.family === 'evm' ? (typeof value === 'string' ? `0x${BigInt(value).toString(16)}` : null) : typeof value === 'string' ? value : null;
297
+ /** Passive checks on results that pass through anyway: no extra network call. Returns false when the endpoint's head is stale. */
298
+ function observe(item, method, params, result) {
299
+ if (method === identityMethod) {
300
+ const ok = identityOf(result) === policy.identity;
301
+ item.identity = ok ? 'verified' : 'mismatch';
302
+ if (!ok)
303
+ exclude(item, 'chain-mismatch', null);
304
+ return true;
305
+ }
306
+ if (policy.family === 'solana' || method !== 'eth_getBlockByNumber' || !result || typeof result !== 'object')
307
+ return true;
308
+ const header = result;
309
+ const number = numericTag(header.number);
310
+ if (number === null || typeof header.hash !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(header.hash))
311
+ return true;
312
+ const hash = header.hash.toLowerCase();
313
+ const prior = anchors.get(number.toString());
314
+ if (prior && prior.hash !== hash) {
315
+ telemetry.crossCheckMismatch(item);
316
+ return true;
317
+ }
318
+ if (!prior)
319
+ anchors.set(number.toString(), { hash, endpoint: item.id });
320
+ // Only the head a run pins (`latest`) is age-checked: a head that stopped advancing is stale. Nothing compares against finality.
321
+ if (params[0] === policy.headTag) {
322
+ const timestamp = numericTag(header.timestamp);
323
+ const age = timestamp === null ? null : Math.floor(now().getTime() / 1000) - Number(timestamp);
324
+ const stale = age !== null && age > policy.maximumHeadAgeSeconds;
325
+ telemetry.freshness(item, { headAgeSeconds: age, thresholdSeconds: policy.maximumHeadAgeSeconds, observedAt: now().toISOString(), stale });
326
+ if (stale) {
327
+ exclude(item, 'provider-stale', 60000);
328
+ anchors.delete(number.toString());
329
+ return false;
330
+ }
331
+ }
332
+ return true;
333
+ }
334
+ return {
335
+ get retryBudgetRemaining() { return retryBudget; },
336
+ servedBy: () => lastServed,
337
+ health: () => state.map((item) => ({ id: item.id, host: item.host, excluded: available(item) ? null : item.excluded.category, identity: item.identity })),
338
+ async request(method, params, signal) {
339
+ const key = cache ? publicCacheKey({ chain: options.chain, family: policy.family, identity: policy.identity, anchors }, method, params) : null;
340
+ if (cache && key) {
341
+ const hit = cache.get(key);
342
+ if (hit !== undefined) {
343
+ telemetry.cacheHit(state[active] ?? null);
344
+ lastServed = cachedFrom.get(key) ?? lastServed;
345
+ return hit;
346
+ }
347
+ telemetry.cacheMiss();
348
+ }
349
+ else if (cache && (PRIVATE_EVM_METHODS.has(method) || PRIVATE_SOLANA_METHODS.has(method)))
350
+ telemetry.cachePrivateRefusal();
351
+ let lastError = new ProviderHealthError('provider-unavailable', 'No healthy configured endpoint is available.');
352
+ let previous = null;
353
+ for (let attempt = 0; attempt < maximumAttempts; attempt++) {
354
+ const serving = state.map((_, offset) => state[(active + offset) % state.length]).filter((candidate) => available(candidate) && (candidate.serves?.(method, params) ?? true));
355
+ // An endpoint that prefers this request (for example the keyed archive provider for a trace or a deep log range) answers it first.
356
+ const ordered = [...serving.filter((candidate) => candidate.prefers?.(method, params)), ...serving.filter((candidate) => !candidate.prefers?.(method, params))];
357
+ if (!ordered.length && attempt === 0 && !state.some((candidate) => candidate.serves?.(method, params) ?? true))
358
+ throw new ProviderHealthError('request-rejected', `No configured ${options.chain} endpoint serves ${method}.`);
359
+ const item = ordered.find((candidate) => candidate !== previous) ?? ordered[0];
360
+ if (!item)
361
+ break;
362
+ if (attempt > 0 && !spend())
363
+ break;
364
+ const failover = previous !== null && item !== previous;
365
+ try {
366
+ if (failover && item.identity === 'unverified' && method !== identityMethod) {
367
+ if (!spend())
368
+ break;
369
+ const identity = await send(item, identityMethod, [], signal, 'identity', true);
370
+ if (!observe(item, identityMethod, [], identity) || item.identity !== 'verified') {
371
+ lastError = new ProviderHealthError('chain-mismatch', 'A failover endpoint reported a different chain identity.');
372
+ previous = item;
373
+ continue;
374
+ }
375
+ }
376
+ const pinned = policy.family === 'evm' ? referencedBlockNumber(method, params) : null;
377
+ const anchor = pinned === null ? undefined : anchors.get(pinned.toString());
378
+ if (anchor && anchor.endpoint !== item.id && !crossChecked.has(`${pinned}:${item.id}`)) {
379
+ if (!spend())
380
+ break;
381
+ const header = await send(item, 'eth_getBlockByNumber', [`0x${pinned.toString(16)}`, false], signal, 'cross-check', true);
382
+ if (typeof header?.hash !== 'string' || header.hash.toLowerCase() !== anchor.hash) {
383
+ telemetry.crossCheckMismatch(item);
384
+ exclude(item, 'snapshot-mismatch', null);
385
+ lastError = new ProviderHealthError('snapshot-mismatch', 'The failover endpoint does not share the pinned block hash; its data was not used.');
386
+ previous = item;
387
+ continue;
388
+ }
389
+ // One matching header check per endpoint and pinned block is enough; later reads at that block reuse it.
390
+ crossChecked.add(`${pinned}:${item.id}`);
391
+ }
392
+ const result = await send(item, method, params, signal, 'request', attempt > 0);
393
+ if (policy.family === 'solana' && method === 'getSlot' && params[0]?.commitment === policy.headTag && !item.freshnessChecked && state.length > 1
394
+ && (typeof result === 'bigint' || typeof result === 'number') && spend()) {
395
+ // A Solana endpoint whose confirmed head stopped advancing is recognised from that slot's block time and dropped before its data is used.
396
+ item.freshnessChecked = true;
397
+ const blockTime = await send(item, 'getBlockTime', [result], signal, 'freshness-probe', true);
398
+ if (typeof blockTime === 'bigint' || typeof blockTime === 'number') {
399
+ const age = Math.floor(now().getTime() / 1000) - Number(blockTime);
400
+ const stale = age > policy.maximumHeadAgeSeconds;
401
+ telemetry.freshness(item, { headAgeSeconds: age, thresholdSeconds: policy.maximumHeadAgeSeconds, observedAt: now().toISOString(), stale });
402
+ if (stale) {
403
+ exclude(item, 'provider-stale', 60000);
404
+ lastError = new ProviderHealthError('provider-stale', `The ${item.host} confirmed slot is ${age} s old.`);
405
+ previous = item;
406
+ continue;
407
+ }
408
+ }
409
+ }
410
+ if (!observe(item, method, params, result)) {
411
+ lastError = new ProviderHealthError('provider-stale', `The ${item.host} latest block exceeds the ${options.chain} head-age threshold.`);
412
+ previous = item;
413
+ continue;
414
+ }
415
+ // A preferred endpoint answering its own class of request is routing, not a failover away from the active endpoint.
416
+ if (failover && !item.prefers?.(method, params)) {
417
+ telemetry.failover(item);
418
+ active = item.index;
419
+ }
420
+ lastServed = { id: item.id, host: item.host };
421
+ if (cache && key) {
422
+ cache.set(key, result);
423
+ cachedFrom.set(key, lastServed);
424
+ if (cachedFrom.size > 512)
425
+ cachedFrom.delete(cachedFrom.keys().next().value);
426
+ telemetry.cacheStore();
427
+ }
428
+ return result;
429
+ }
430
+ catch (error) {
431
+ const category = categorize(error, signal);
432
+ if (!RETRYABLE.has(category) || signal.aborted)
433
+ throw error;
434
+ lastError = error;
435
+ previous = item;
436
+ if (state.filter(available).length > 1 && category !== 'history-unavailable')
437
+ exclude(item, category, 15000);
438
+ if (options.backoffMs && attempt + 1 < maximumAttempts)
439
+ await delay(options.backoffMs * (attempt + 1), undefined, { signal }).catch(() => { throw error; });
440
+ }
441
+ }
442
+ throw lastError;
443
+ },
444
+ };
445
+ }
446
+ /** Active three-call head freshness probe for operators and verification scripts: identity, the head and its age. No finality is read. */
447
+ export async function probeFreshness(chain, endpoint, options = {}) {
448
+ const policy = HEAD_FRESHNESS_POLICIES[chain];
449
+ if (!policy)
450
+ throw new ProviderHealthError('invalid-response', `No head freshness policy is registered for ${chain}.`);
451
+ const now = options.now ?? (() => new Date());
452
+ const signal = options.signal ?? AbortSignal.timeout(20000);
453
+ const telemetry = options.telemetry ?? PROVIDER_TELEMETRY;
454
+ const item = { id: endpoint.id, chain, host: endpointHost(endpoint.url) };
455
+ const observation = { chain, host: item.host, observedAt: now().toISOString(), status: 'healthy', identity: null, expectedIdentity: policy.identity, tag: policy.headTag, head: null, headAgeSeconds: null, thresholdSeconds: policy.maximumHeadAgeSeconds, requests: 0 };
456
+ const call = async (method, params) => {
457
+ observation.requests++;
458
+ const started = performance.now();
459
+ try {
460
+ const result = await endpoint.transport.request(method, params, signal);
461
+ telemetry.attempt(item, { method, outcome: 'success', latencyMs: performance.now() - started, purpose: 'freshness-probe', at: now() });
462
+ return result;
463
+ }
464
+ catch (error) {
465
+ telemetry.attempt(item, { method, outcome: categorizeProviderError(error, signal), latencyMs: performance.now() - started, purpose: 'freshness-probe', at: now() });
466
+ throw error;
467
+ }
468
+ };
469
+ try {
470
+ const identity = await call(policy.family === 'evm' ? 'eth_chainId' : 'getGenesisHash', []);
471
+ observation.identity = policy.family === 'evm' && typeof identity === 'string' ? `0x${BigInt(identity).toString(16)}` : String(identity);
472
+ if (observation.identity !== policy.identity) {
473
+ observation.status = 'chain-mismatch';
474
+ telemetry.mark(item, 'excluded', 'chain-mismatch');
475
+ return observation;
476
+ }
477
+ if (policy.family === 'evm') {
478
+ const block = await call('eth_getBlockByNumber', [policy.headTag, false]);
479
+ const number = numericTag(block?.number);
480
+ if (number === null)
481
+ throw new ProviderHealthError('invalid-response', 'Malformed block header.');
482
+ const timestamp = numericTag(block?.timestamp);
483
+ observation.head = { number: number.toString(), hash: typeof block?.hash === 'string' ? block.hash.toLowerCase() : null, timestamp: timestamp?.toString() ?? null };
484
+ if (timestamp !== null)
485
+ observation.headAgeSeconds = Math.floor(now().getTime() / 1000) - Number(timestamp);
486
+ }
487
+ else {
488
+ const slot = await call('getSlot', [{ commitment: policy.headTag }]);
489
+ if (typeof slot !== 'bigint' && typeof slot !== 'number')
490
+ throw new ProviderHealthError('invalid-response', 'Malformed slot.');
491
+ const blockTime = await call('getBlockTime', [slot]);
492
+ observation.head = { number: BigInt(slot).toString(), hash: null, timestamp: typeof blockTime === 'bigint' || typeof blockTime === 'number' ? BigInt(blockTime).toString() : null };
493
+ if (observation.head.timestamp !== null)
494
+ observation.headAgeSeconds = Math.floor(now().getTime() / 1000) - Number(observation.head.timestamp);
495
+ }
496
+ const stale = observation.headAgeSeconds !== null && observation.headAgeSeconds > policy.maximumHeadAgeSeconds;
497
+ if (stale)
498
+ observation.status = 'provider-stale';
499
+ telemetry.freshness(item, { headAgeSeconds: observation.headAgeSeconds, thresholdSeconds: policy.maximumHeadAgeSeconds, observedAt: observation.observedAt, stale });
500
+ }
501
+ catch (error) {
502
+ observation.status = categorizeProviderError(error, signal);
503
+ }
504
+ return observation;
505
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Server-side keyed provider transports (E03). Keys come only from the server environment, are embedded only in the outbound
3
+ * request URL, and are never returned, logged or placed in errors or evidence: failures carry a category and a fixed message.
4
+ * Every request reserves provider units against a per-run budget and a process-wide daily cap well inside the free tier, is
5
+ * spaced to a conservative local request rate, and a 429 gets at most one bounded retry.
6
+ */
7
+ export type KeyedProviderId = 'alchemy' | 'helius';
8
+ export type KeyedFailure = 'credential-required' | 'unsupported' | 'history-unavailable' | 'budget-exhausted' | 'rate-limited' | 'plan-restricted' | 'provider-unavailable' | 'invalid-response' | 'request-rejected' | 'execution-result' | 'timeout' | 'cancelled' | 'limit-exceeded';
9
+ export declare class KeyedProviderError extends Error {
10
+ readonly category: KeyedFailure;
11
+ readonly status: number | null;
12
+ readonly rpcCode: number | null;
13
+ constructor(category: KeyedFailure, message: string, status?: number | null, rpcCode?: number | null);
14
+ }
15
+ export interface KeyedProviderPolicy {
16
+ id: KeyedProviderId;
17
+ credentialReference: 'ALCHEMY_API_KEY' | 'HELIUS_API_KEY';
18
+ unit: 'compute-unit' | 'credit';
19
+ hosts: Readonly<Record<string, string>>;
20
+ unitCosts: Readonly<Record<string, number>>;
21
+ defaultUnitCost: number;
22
+ perRunUnits: number;
23
+ perDayUnits: number;
24
+ minimumSpacingMs: Readonly<Record<string, number>>;
25
+ defaultSpacingMs: number;
26
+ freeTier: string;
27
+ terms: string;
28
+ checkedAt: string;
29
+ /** How units are billed: Alchemy is on pay-as-you-go (owner-confirmed 26 Sep 2026); Helius stays on its free tier. */ billing: 'pay-as-you-go-units-metered' | 'free-tier-units-not-invoiced';
30
+ }
31
+ /** Free-tier figures and terms were read from provider pricing, CU-cost and legal pages on 2026-09-14. Caps stay far below them. */
32
+ export declare const KEYED_PROVIDER_POLICIES: Readonly<Record<KeyedProviderId, KeyedProviderPolicy>>;
33
+ /** Process-wide usage. It holds counters only: no request parameters, results, tenants or credentials. */
34
+ export declare class KeyedUsageLedger {
35
+ #private;
36
+ reserve(policy: KeyedProviderPolicy, units: number, at: Date): void;
37
+ space(policy: KeyedProviderPolicy, method: string, signal: AbortSignal, now: () => number): Promise<void>;
38
+ count(provider: KeyedProviderId, event: {
39
+ units?: number;
40
+ rateLimited?: boolean;
41
+ failed?: boolean;
42
+ }): void;
43
+ snapshot(at?: Date): {
44
+ costUsd: null;
45
+ costStatus: "pay-as-you-go-units-metered" | "free-tier-units-not-invoiced";
46
+ requests: number;
47
+ units: number;
48
+ rateLimited: number;
49
+ failures: number;
50
+ provider: KeyedProviderId;
51
+ unit: "compute-unit" | "credit";
52
+ dayUnitsUsed: number;
53
+ dayUnitsCap: number;
54
+ }[];
55
+ }
56
+ export declare const KEYED_USAGE: KeyedUsageLedger;
57
+ /** Server wiring replaces the process-local default with the database-backed ledger so the daily cap is shared and survives restarts. */
58
+ export declare const setKeyedUsageLedger: (ledger: KeyedUsageLedger) => void;
59
+ export declare const activeKeyedUsage: () => KeyedUsageLedger;
60
+ export declare class RunUnitBudget {
61
+ #private;
62
+ readonly maximum: number;
63
+ constructor(maximum: number);
64
+ get used(): number;
65
+ reserve(units: number): void;
66
+ }
67
+ export declare const keyedCredentialAvailable: (provider: KeyedProviderId, env?: Record<string, string | undefined>) => boolean;
68
+ export declare const keyedProviderHost: (provider: KeyedProviderId, chain: string) => string | null;
69
+ /** Removes any configured credential from text before it can reach a log, error or artifact. */
70
+ export declare function redactCredentials(text: string, env?: Record<string, string | undefined>): string;
71
+ export interface KeyedTransportOptions {
72
+ env?: Record<string, string | undefined>;
73
+ fetch?: typeof fetch;
74
+ usage?: KeyedUsageLedger;
75
+ now?: () => Date;
76
+ timeoutMs?: number;
77
+ maximumResponseBytes?: number;
78
+ /** Mandatory: every keyed transport belongs to one run's unit budget, at most the provider's per-run policy. */
79
+ runBudget: RunUnitBudget;
80
+ /** 0 hands a 429 straight to the caller (for example a failover transport); 1, the default, makes one bounded retry. */
81
+ maximumRetries?: 0 | 1;
82
+ /** Optional durable permit hook for each network attempt, including a 429 retry. */
83
+ beforeAttempt?: (operation: {
84
+ provider: KeyedProviderId;
85
+ method: string;
86
+ attempt: number;
87
+ units: number;
88
+ }) => void | Promise<void>;
89
+ }
90
+ export interface KeyedTransport {
91
+ readonly provider: KeyedProviderId;
92
+ readonly host: string;
93
+ request(method: string, params: readonly unknown[], signal: AbortSignal): Promise<unknown>;
94
+ readonly unitsUsed: number;
95
+ }
96
+ export declare function createKeyedTransport(provider: KeyedProviderId, chain: string, options: KeyedTransportOptions): KeyedTransport;