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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/dist/packages/contracts/src/deployer-investigation.d.ts +1015 -0
- package/dist/packages/contracts/src/deployer-investigation.js +101 -0
- package/dist/packages/contracts/src/index.d.ts +1701 -0
- package/dist/packages/contracts/src/index.js +380 -0
- package/dist/packages/contracts/src/indexed-activity.d.ts +684 -0
- package/dist/packages/contracts/src/indexed-activity.js +71 -0
- package/dist/packages/contracts/src/indexed-agents.d.ts +299 -0
- package/dist/packages/contracts/src/indexed-agents.js +131 -0
- package/dist/packages/contracts/src/inspection.d.ts +906 -0
- package/dist/packages/contracts/src/inspection.js +114 -0
- package/dist/packages/contracts/src/kinds.d.ts +5398 -0
- package/dist/packages/contracts/src/kinds.js +156 -0
- package/dist/packages/contracts/src/report-presentation.d.ts +346 -0
- package/dist/packages/contracts/src/report-presentation.js +120 -0
- package/dist/packages/contracts/src/solana-inspection.d.ts +451 -0
- package/dist/packages/contracts/src/solana-inspection.js +94 -0
- package/dist/packages/contracts/src/token-market.d.ts +193 -0
- package/dist/packages/contracts/src/token-market.js +335 -0
- package/dist/packages/contracts/src/wallet-analysis.d.ts +866 -0
- package/dist/packages/contracts/src/wallet-analysis.js +89 -0
- package/dist/packages/contracts/src/watchtower.d.ts +1141 -0
- package/dist/packages/contracts/src/watchtower.js +196 -0
- package/dist/packages/contracts/src/workflow.d.ts +1568 -0
- package/dist/packages/contracts/src/workflow.js +651 -0
- package/dist/packages/inspector/src/decode.d.ts +23 -0
- package/dist/packages/inspector/src/decode.js +150 -0
- package/dist/packages/inspector/src/scope.d.ts +87 -0
- package/dist/packages/inspector/src/scope.js +64 -0
- package/dist/packages/model/src/analysis.d.ts +149 -0
- package/dist/packages/model/src/analysis.js +387 -0
- package/dist/packages/model/src/pricing.d.ts +38 -0
- package/dist/packages/model/src/pricing.js +49 -0
- package/dist/packages/model/src/retry.d.ts +20 -0
- package/dist/packages/model/src/retry.js +31 -0
- package/dist/packages/model/src/schema.d.ts +10 -0
- package/dist/packages/model/src/schema.js +51 -0
- package/dist/packages/model/src/summary.d.ts +91 -0
- package/dist/packages/model/src/summary.js +177 -0
- package/dist/packages/model/src/types.d.ts +81 -0
- package/dist/packages/model/src/types.js +19 -0
- package/dist/packages/monitoring/src/delivery.d.ts +32 -0
- package/dist/packages/monitoring/src/delivery.js +53 -0
- package/dist/packages/providers/src/chain-transport.d.ts +42 -0
- package/dist/packages/providers/src/chain-transport.js +57 -0
- package/dist/packages/providers/src/coverage.d.ts +105 -0
- package/dist/packages/providers/src/coverage.js +260 -0
- package/dist/packages/providers/src/health.d.ts +273 -0
- package/dist/packages/providers/src/health.js +505 -0
- package/dist/packages/providers/src/keyed.d.ts +96 -0
- package/dist/packages/providers/src/keyed.js +240 -0
- package/dist/packages/providers/src/snapshot.d.ts +61 -0
- package/dist/packages/providers/src/snapshot.js +77 -0
- package/dist/packages/publication/src/fixtures.d.ts +45 -0
- package/dist/packages/publication/src/fixtures.js +350 -0
- package/dist/packages/research/src/index.d.ts +188 -0
- package/dist/packages/research/src/index.js +829 -0
- package/dist/packages/runtime/src/checkpoints.d.ts +65 -0
- package/dist/packages/runtime/src/checkpoints.js +214 -0
- package/dist/packages/runtime/src/policy.d.ts +57 -0
- package/dist/packages/runtime/src/policy.js +296 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex-buyer.js +4 -0
- package/dist/packages/sdk/src/bin/agentex.d.ts +2 -0
- package/dist/packages/sdk/src/bin/agentex.js +3 -0
- package/dist/packages/sdk/src/buyer-cli.d.ts +10 -0
- package/dist/packages/sdk/src/buyer-cli.js +210 -0
- package/dist/packages/sdk/src/buyer.d.ts +534 -0
- package/dist/packages/sdk/src/buyer.js +441 -0
- package/dist/packages/sdk/src/cli.d.ts +14 -0
- package/dist/packages/sdk/src/cli.js +149 -0
- package/dist/packages/sdk/src/errors.d.ts +31 -0
- package/dist/packages/sdk/src/errors.js +24 -0
- package/dist/packages/sdk/src/index.d.ts +236 -0
- package/dist/packages/sdk/src/index.js +150 -0
- package/dist/packages/sdk/src/local.d.ts +11 -0
- package/dist/packages/sdk/src/local.js +110 -0
- package/dist/packages/sdk/src/rails.d.ts +59 -0
- package/dist/packages/sdk/src/rails.js +96 -0
- package/dist/packages/sdk/src/report.d.ts +121 -0
- package/dist/packages/sdk/src/report.js +114 -0
- package/dist/packages/sdk/src/version.d.ts +2 -0
- package/dist/packages/sdk/src/version.js +2 -0
- package/dist/packages/watchtower/src/index.d.ts +150 -0
- package/dist/packages/watchtower/src/index.js +786 -0
- package/dist/packages/workflow/src/registry.d.ts +61 -0
- package/dist/packages/workflow/src/registry.js +76 -0
- package/examples/README.md +34 -0
- package/examples/cli-usage.sh +30 -0
- package/examples/fixtures/base-weth-input.json +4 -0
- package/examples/focused-researcher.json +127 -0
- package/examples/pay-with-eth-robinhood.mts +37 -0
- package/examples/pay-with-usdc.mts +41 -0
- package/examples/quickstart.mts +73 -0
- package/package.json +50 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { parseJsonWithBigInts, stringifyJsonWithBigInts } from '@solana/rpc-spec-types';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export class KeyedProviderError extends Error {
|
|
5
|
+
category;
|
|
6
|
+
status;
|
|
7
|
+
rpcCode;
|
|
8
|
+
constructor(category, message, status = null, rpcCode = null) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.category = category;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.rpcCode = rpcCode;
|
|
13
|
+
this.name = 'KeyedProviderError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Free-tier figures and terms were read from provider pricing, CU-cost and legal pages on 2026-09-14. Caps stay far below them. */
|
|
17
|
+
export const KEYED_PROVIDER_POLICIES = Object.freeze({
|
|
18
|
+
alchemy: Object.freeze({
|
|
19
|
+
id: 'alchemy', credentialReference: 'ALCHEMY_API_KEY', unit: 'compute-unit', hosts: Object.freeze({ ethereum: 'eth-mainnet.g.alchemy.com', base: 'base-mainnet.g.alchemy.com', robinhood: 'robinhood-mainnet.g.alchemy.com', 'robinhood-testnet': 'robinhood-testnet.g.alchemy.com' }),
|
|
20
|
+
unitCosts: Object.freeze({ eth_chainId: 0, eth_getBlockByNumber: 20, eth_getBlockByHash: 20, eth_call: 26, eth_getLogs: 60, eth_getCode: 20, eth_getStorageAt: 20, eth_getBalance: 20, eth_getTransactionCount: 20, eth_getTransactionByHash: 20, eth_getTransactionReceipt: 20, debug_traceTransaction: 40, trace_filter: 40, alchemy_getAssetTransfers: 120, alchemy_getTokenBalances: 20, alchemy_getTokenMetadata: 10, alchemy_getContractMetadata: 160 }),
|
|
21
|
+
// Pay-as-you-go at $0.525 per 1M CU: 3M CU a day caps spend near $1.60 a day across every run, while one step stays within 2,000 CU.
|
|
22
|
+
defaultUnitCost: 60, perRunUnits: 2000, perDayUnits: 3000000, minimumSpacingMs: Object.freeze({}), defaultSpacingMs: 100,
|
|
23
|
+
freeTier: 'Pay-as-you-go (owner-confirmed 26 Sep 2026): $0.525 per 1M CU, 300 requests/second (alchemy.com/pricing). CU costs from alchemy.com/docs/reference/compute-unit-costs, read 26 Sep 2026.',
|
|
24
|
+
terms: 'legal.alchemy.com, plus written approval from Alchemy (September 2026) for AGENTEX to use Alchemy data in its paid reports, exports and shares. Buyers never get pass-through access to Alchemy; proprietary notices are kept.', checkedAt: '2026-09-26T00:00:00.000Z', billing: 'pay-as-you-go-units-metered'
|
|
25
|
+
}),
|
|
26
|
+
helius: Object.freeze({
|
|
27
|
+
id: 'helius', credentialReference: 'HELIUS_API_KEY', unit: 'credit', hosts: Object.freeze({ solana: 'mainnet.helius-rpc.com' }),
|
|
28
|
+
unitCosts: Object.freeze({ getGenesisHash: 1, getSlot: 1, getAccountInfo: 1, getTransaction: 10, getSignaturesForAddress: 1, getTransactionsForAddress: 10, getTokenAccounts: 10, getAsset: 10 }),
|
|
29
|
+
defaultUnitCost: 10, perRunUnits: 200, perDayUnits: 15000, minimumSpacingMs: Object.freeze({ getTokenAccounts: 1000, getAsset: 1000 }), defaultSpacingMs: 250,
|
|
30
|
+
freeTier: 'Free: 1M credits/month, 10 RPC requests/second, 2 DAS requests/second (helius.dev/pricing). Standard RPC 1 credit; archival and DAS 10; getTransactionsForAddress 10 credits per call (signatures) or per 100 transactions (full).',
|
|
31
|
+
billing: 'free-tier-units-not-invoiced',
|
|
32
|
+
terms: 'helius.dev/terms: no sublicensing, reselling, distributing or otherwise allowing use of the products for the benefit of third parties. Treated as internal evaluation only until an explicit licence review.', checkedAt: '2026-09-14T00:00:00.000Z'
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
const dayOf = (at) => at.toISOString().slice(0, 10);
|
|
36
|
+
/** Process-wide usage. It holds counters only: no request parameters, results, tenants or credentials. */
|
|
37
|
+
export class KeyedUsageLedger {
|
|
38
|
+
#days = new Map();
|
|
39
|
+
#last = new Map();
|
|
40
|
+
#totals = new Map();
|
|
41
|
+
reserve(policy, units, at) {
|
|
42
|
+
const key = `${policy.id}:${dayOf(at)}`;
|
|
43
|
+
const used = this.#days.get(key) ?? 0;
|
|
44
|
+
if (used + units > policy.perDayUnits)
|
|
45
|
+
throw new KeyedProviderError('budget-exhausted', `The daily ${policy.id} ${policy.unit} cap is reached.`);
|
|
46
|
+
this.#days.set(key, used + units);
|
|
47
|
+
for (const stale of this.#days.keys())
|
|
48
|
+
if (!stale.endsWith(dayOf(at)) && stale.startsWith(`${policy.id}:`))
|
|
49
|
+
this.#days.delete(stale);
|
|
50
|
+
}
|
|
51
|
+
async space(policy, method, signal, now) {
|
|
52
|
+
const gap = policy.minimumSpacingMs[method] ?? policy.defaultSpacingMs;
|
|
53
|
+
const key = `${policy.id}:${gap === policy.defaultSpacingMs ? 'default' : method}`;
|
|
54
|
+
const wait = (this.#last.get(key) ?? 0) + gap - now();
|
|
55
|
+
this.#last.set(key, Math.max(now(), (this.#last.get(key) ?? 0) + gap));
|
|
56
|
+
if (wait > 0)
|
|
57
|
+
await delay(wait, undefined, { signal });
|
|
58
|
+
}
|
|
59
|
+
count(provider, event) {
|
|
60
|
+
const entry = this.#totals.get(provider) ?? { requests: 0, units: 0, rateLimited: 0, failures: 0 };
|
|
61
|
+
entry.requests++;
|
|
62
|
+
entry.units += event.units ?? 0;
|
|
63
|
+
if (event.rateLimited)
|
|
64
|
+
entry.rateLimited++;
|
|
65
|
+
if (event.failed)
|
|
66
|
+
entry.failures++;
|
|
67
|
+
this.#totals.set(provider, entry);
|
|
68
|
+
}
|
|
69
|
+
snapshot(at = new Date()) {
|
|
70
|
+
return Object.keys(KEYED_PROVIDER_POLICIES).map((id) => ({ provider: id, unit: KEYED_PROVIDER_POLICIES[id].unit, dayUnitsUsed: this.#days.get(`${id}:${dayOf(at)}`) ?? 0, dayUnitsCap: KEYED_PROVIDER_POLICIES[id].perDayUnits, ...(this.#totals.get(id) ?? { requests: 0, units: 0, rateLimited: 0, failures: 0 }), costUsd: null, costStatus: KEYED_PROVIDER_POLICIES[id].billing }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const KEYED_USAGE = new KeyedUsageLedger();
|
|
74
|
+
let activeUsage = KEYED_USAGE;
|
|
75
|
+
/** Server wiring replaces the process-local default with the database-backed ledger so the daily cap is shared and survives restarts. */
|
|
76
|
+
export const setKeyedUsageLedger = (ledger) => { activeUsage = ledger; };
|
|
77
|
+
export const activeKeyedUsage = () => activeUsage;
|
|
78
|
+
export class RunUnitBudget {
|
|
79
|
+
maximum;
|
|
80
|
+
#used = 0;
|
|
81
|
+
constructor(maximum) {
|
|
82
|
+
this.maximum = maximum;
|
|
83
|
+
z.number().int().min(1).max(100000).parse(maximum);
|
|
84
|
+
}
|
|
85
|
+
get used() { return this.#used; }
|
|
86
|
+
reserve(units) { if (this.#used + units > this.maximum)
|
|
87
|
+
throw new KeyedProviderError('budget-exhausted', 'The per-run provider unit budget is exhausted.'); this.#used += units; }
|
|
88
|
+
}
|
|
89
|
+
export const keyedCredentialAvailable = (provider, env = process.env) => /^[A-Za-z0-9_-]{16,128}$/.test(env[KEYED_PROVIDER_POLICIES[provider].credentialReference] ?? '');
|
|
90
|
+
export const keyedProviderHost = (provider, chain) => KEYED_PROVIDER_POLICIES[provider].hosts[chain] ?? null;
|
|
91
|
+
/** Removes any configured credential from text before it can reach a log, error or artifact. */
|
|
92
|
+
export function redactCredentials(text, env = process.env) {
|
|
93
|
+
let result = text;
|
|
94
|
+
for (const policy of Object.values(KEYED_PROVIDER_POLICIES)) {
|
|
95
|
+
const value = env[policy.credentialReference];
|
|
96
|
+
if (value && value.length >= 8)
|
|
97
|
+
result = result.split(value).join(`[${policy.credentialReference}]`);
|
|
98
|
+
}
|
|
99
|
+
return result.replace(/api-key=[^&\s"']+/gi, 'api-key=[redacted]').replace(/\/v2\/[A-Za-z0-9_-]{16,}/g, '/v2/[redacted]').replace(/\/nft\/v3\/[A-Za-z0-9_-]{16,}/g, '/nft/v3/[redacted]');
|
|
100
|
+
}
|
|
101
|
+
async function readBounded(response, maximum) {
|
|
102
|
+
const reader = response.body?.getReader();
|
|
103
|
+
if (!reader)
|
|
104
|
+
return '';
|
|
105
|
+
const chunks = [];
|
|
106
|
+
let size = 0;
|
|
107
|
+
try {
|
|
108
|
+
for (;;) {
|
|
109
|
+
const chunk = await reader.read();
|
|
110
|
+
if (chunk.done)
|
|
111
|
+
break;
|
|
112
|
+
size += chunk.value.length;
|
|
113
|
+
if (size > maximum) {
|
|
114
|
+
await reader.cancel();
|
|
115
|
+
throw new KeyedProviderError('limit-exceeded', 'The provider response exceeds the byte limit.');
|
|
116
|
+
}
|
|
117
|
+
chunks.push(chunk.value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
reader.releaseLock();
|
|
122
|
+
}
|
|
123
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
124
|
+
}
|
|
125
|
+
export function createKeyedTransport(provider, chain, options) {
|
|
126
|
+
const policy = KEYED_PROVIDER_POLICIES[provider];
|
|
127
|
+
const host = keyedProviderHost(provider, chain);
|
|
128
|
+
if (!host)
|
|
129
|
+
throw new KeyedProviderError('unsupported', `${provider} is not configured for ${chain}.`);
|
|
130
|
+
if (!(options?.runBudget instanceof RunUnitBudget))
|
|
131
|
+
throw new KeyedProviderError('budget-exhausted', 'Every keyed provider transport needs a per-run unit budget.');
|
|
132
|
+
if (options.runBudget.maximum > policy.perRunUnits)
|
|
133
|
+
throw new KeyedProviderError('limit-exceeded', `The per-run budget exceeds the ${provider} policy of ${policy.perRunUnits} ${policy.unit}s.`);
|
|
134
|
+
const env = options.env ?? process.env;
|
|
135
|
+
const usage = options.usage ?? activeUsage;
|
|
136
|
+
const now = options.now ?? (() => new Date());
|
|
137
|
+
const attemptsAllowed = 1 + (options.maximumRetries ?? 1);
|
|
138
|
+
const request = options.fetch ?? fetch;
|
|
139
|
+
const maximumBytes = options.maximumResponseBytes ?? 1048576;
|
|
140
|
+
let unitsUsed = 0;
|
|
141
|
+
let attempts = 0;
|
|
142
|
+
return {
|
|
143
|
+
provider, host, get unitsUsed() { return unitsUsed; },
|
|
144
|
+
async request(method, params, signal) {
|
|
145
|
+
const key = env[policy.credentialReference];
|
|
146
|
+
if (!keyedCredentialAvailable(provider, env) || !key)
|
|
147
|
+
throw new KeyedProviderError('credential-required', `${policy.credentialReference} is not configured on the server.`);
|
|
148
|
+
const units = policy.unitCosts[method] ?? policy.defaultUnitCost;
|
|
149
|
+
for (let retry = 0; retry < attemptsAllowed; retry++) {
|
|
150
|
+
if (signal.aborted)
|
|
151
|
+
throw new KeyedProviderError(signal.reason?.name === 'TimeoutError' ? 'timeout' : 'cancelled', 'The request was interrupted.');
|
|
152
|
+
options.runBudget.reserve(units);
|
|
153
|
+
await usage.reserve(policy, units, now());
|
|
154
|
+
await options.beforeAttempt?.({ provider, method, attempt: ++attempts, units });
|
|
155
|
+
await usage.space(policy, method, signal, () => Date.now());
|
|
156
|
+
unitsUsed += units;
|
|
157
|
+
let url;
|
|
158
|
+
let init;
|
|
159
|
+
if (provider === 'alchemy' && method === 'alchemy_getContractMetadata') {
|
|
160
|
+
const contract = String(params[0]);
|
|
161
|
+
if (!/^0x[a-fA-F0-9]{40}$/.test(contract))
|
|
162
|
+
throw new KeyedProviderError('request-rejected', 'A contract address is required.');
|
|
163
|
+
url = `https://${host}/nft/v3/${key}/getContractMetadata?contractAddress=${contract}`;
|
|
164
|
+
init = { method: 'GET', headers: { accept: 'application/json' } };
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
url = provider === 'alchemy' ? `https://${host}/v2/${key}` : `https://${host}/?api-key=${key}`;
|
|
168
|
+
const payload = { jsonrpc: '2.0', id: 1, method, params: provider === 'helius' && ['getTokenAccounts', 'getAsset'].includes(method) ? params[0] : params };
|
|
169
|
+
init = { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: stringifyJsonWithBigInts(payload) };
|
|
170
|
+
}
|
|
171
|
+
let response;
|
|
172
|
+
try {
|
|
173
|
+
response = await request(url, { ...init, redirect: 'error', signal: AbortSignal.any([signal, AbortSignal.timeout(options.timeoutMs ?? 10000)]) });
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
usage.count(provider, { units, failed: true });
|
|
177
|
+
if (signal.aborted)
|
|
178
|
+
throw new KeyedProviderError(signal.reason?.name === 'TimeoutError' ? 'timeout' : 'cancelled', 'The request was interrupted.');
|
|
179
|
+
throw new KeyedProviderError('provider-unavailable', `${provider} could not be reached.`);
|
|
180
|
+
}
|
|
181
|
+
if (response.status === 429) {
|
|
182
|
+
usage.count(provider, { units, rateLimited: true });
|
|
183
|
+
await response.body?.cancel().catch(() => undefined);
|
|
184
|
+
const retryAfter = Math.min(5, Math.max(1, Number.parseInt(response.headers.get('retry-after') ?? '1', 10) || 1));
|
|
185
|
+
if (retry + 1 < attemptsAllowed) {
|
|
186
|
+
await delay(retryAfter * 1000, undefined, { signal });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
throw new KeyedProviderError('rate-limited', `${provider} rate limited the request after one bounded retry.`, 429);
|
|
190
|
+
}
|
|
191
|
+
let text;
|
|
192
|
+
try {
|
|
193
|
+
text = await readBounded(response, maximumBytes);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
usage.count(provider, { units, failed: true });
|
|
197
|
+
if (error instanceof KeyedProviderError)
|
|
198
|
+
throw error;
|
|
199
|
+
throw new KeyedProviderError('provider-unavailable', `${provider} returned an unreadable body.`);
|
|
200
|
+
}
|
|
201
|
+
if (response.status === 401 || response.status === 403) {
|
|
202
|
+
usage.count(provider, { units, failed: true });
|
|
203
|
+
throw new KeyedProviderError(/plan|upgrade|not available|not enabled|tier/i.test(text) ? 'plan-restricted' : 'credential-required', `${provider} refused this method for the configured key or plan.`, response.status);
|
|
204
|
+
}
|
|
205
|
+
if (response.status === 404 && provider === 'alchemy') {
|
|
206
|
+
usage.count(provider, { units, failed: true });
|
|
207
|
+
throw new KeyedProviderError('unsupported', `${provider} does not serve this method on ${chain}.`, 404);
|
|
208
|
+
}
|
|
209
|
+
let body;
|
|
210
|
+
try {
|
|
211
|
+
body = (provider === 'helius' ? parseJsonWithBigInts(text) : JSON.parse(text));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
usage.count(provider, { units, failed: true });
|
|
215
|
+
throw new KeyedProviderError(response.ok ? 'invalid-response' : 'provider-unavailable', `${provider} returned a non-JSON response (HTTP ${response.status}).`, response.status);
|
|
216
|
+
}
|
|
217
|
+
if (!response.ok && !body.error) {
|
|
218
|
+
usage.count(provider, { units, failed: true });
|
|
219
|
+
throw new KeyedProviderError('provider-unavailable', `${provider} returned HTTP ${response.status}.`, response.status);
|
|
220
|
+
}
|
|
221
|
+
if (method === 'alchemy_getContractMetadata' && provider === 'alchemy') {
|
|
222
|
+
usage.count(provider, { units });
|
|
223
|
+
return body;
|
|
224
|
+
}
|
|
225
|
+
if (body.error) {
|
|
226
|
+
usage.count(provider, { units, failed: true });
|
|
227
|
+
const code = typeof body.error.code === 'number' ? body.error.code : null;
|
|
228
|
+
const message = typeof body.error.message === 'string' ? body.error.message.toLowerCase() : '';
|
|
229
|
+
const category = code === 429 || code === -32005 || /rate limit|too many/.test(message) ? 'rate-limited' : /plan|upgrade|not available on|paid|tier/.test(message) || code === -32403 ? 'plan-restricted'
|
|
230
|
+
: /header not found|unknown block|not currently canonical|missing trie/.test(message) ? 'history-unavailable'
|
|
231
|
+
: code === 3 || /revert/.test(message) ? 'execution-result' : code === -32601 ? 'unsupported' : code === -32602 ? 'request-rejected' : 'provider-unavailable';
|
|
232
|
+
throw new KeyedProviderError(category, `${provider} returned a JSON-RPC error (${category}).`, response.status, code);
|
|
233
|
+
}
|
|
234
|
+
usage.count(provider, { units });
|
|
235
|
+
return body.result;
|
|
236
|
+
}
|
|
237
|
+
throw new KeyedProviderError('rate-limited', `${provider} rate limited the request.`, 429);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-chain read-snapshot policy. Owner rule (2026-09-15): no part of the project waits for finality; reads are instant. Every EVM
|
|
3
|
+
* run reads the provider's `latest` block (sequencer-confirmed), pins it for the whole run and re-reads that height at completion:
|
|
4
|
+
* Ethereum and Base name the pinned block number in every state read, Robinhood Chain names the pinned block hash (EIP-1898
|
|
5
|
+
* `requireCanonical`). A changed hash marks the snapshot `changed`, so a reorg becomes an explicit failure or correction, never a wait.
|
|
6
|
+
* The confirmation level is always reported and never presented as finality. `finalized` and `safe` remain in the confirmation type
|
|
7
|
+
* only so reports captured before the rule still parse and render honestly; no policy reads them.
|
|
8
|
+
*/
|
|
9
|
+
export type SnapshotTag = 'latest';
|
|
10
|
+
export type ReadConfirmation = 'finalized' | 'safe' | 'sequencer-confirmed';
|
|
11
|
+
export interface ReadSnapshotPolicy {
|
|
12
|
+
chain: string;
|
|
13
|
+
chainId: number;
|
|
14
|
+
tags: readonly SnapshotTag[];
|
|
15
|
+
pinBy: 'number' | 'hash';
|
|
16
|
+
confirmationOf: Readonly<Record<SnapshotTag, ReadConfirmation>>;
|
|
17
|
+
basis: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const CONFIRMATION_NOTES: Readonly<Record<ReadConfirmation, string>>;
|
|
20
|
+
export declare const READ_SNAPSHOT_POLICIES: Readonly<Record<string, ReadSnapshotPolicy>>;
|
|
21
|
+
export declare class ReadSnapshotError extends Error {
|
|
22
|
+
readonly category: 'unsupported' | 'chain-mismatch' | 'invalid-response' | 'snapshot-changed' | 'provider-unavailable';
|
|
23
|
+
constructor(category: 'unsupported' | 'chain-mismatch' | 'invalid-response' | 'snapshot-changed' | 'provider-unavailable', message: string);
|
|
24
|
+
}
|
|
25
|
+
export declare function readSnapshotPolicy(chain: string): ReadSnapshotPolicy;
|
|
26
|
+
/** The block parameter for state reads: the pinned block number, or the pinned canonical block hash on hash-pinned chains. */
|
|
27
|
+
export declare function snapshotBlockTag(block: {
|
|
28
|
+
blockNumber: string;
|
|
29
|
+
blockHash: string;
|
|
30
|
+
pinBy: 'number' | 'hash';
|
|
31
|
+
}): string | {
|
|
32
|
+
blockHash: string;
|
|
33
|
+
requireCanonical: true;
|
|
34
|
+
};
|
|
35
|
+
export interface ChainReadTransport {
|
|
36
|
+
request(method: string, params: readonly unknown[], signal: AbortSignal): Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
export interface PinnedReadSnapshot {
|
|
39
|
+
chain: string;
|
|
40
|
+
chainId: number;
|
|
41
|
+
blockNumber: string;
|
|
42
|
+
blockHash: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
tag: SnapshotTag;
|
|
45
|
+
confirmation: ReadConfirmation;
|
|
46
|
+
note: string;
|
|
47
|
+
blockTag: string | {
|
|
48
|
+
blockHash: string;
|
|
49
|
+
requireCanonical: true;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Chain-read helper for services outside the tool recorder: verifies chain identity, pins the latest block under the chain's policy
|
|
54
|
+
* and returns the block tag every later read must use.
|
|
55
|
+
*/
|
|
56
|
+
export declare function pinReadSnapshot(chain: string, transport: ChainReadTransport, signal?: AbortSignal): Promise<PinnedReadSnapshot>;
|
|
57
|
+
/** Rechecks that the pinned hash is still canonical at its height. `changed` means every read made at the snapshot must be discarded. */
|
|
58
|
+
export declare function recheckReadSnapshot(pinned: PinnedReadSnapshot, transport: ChainReadTransport, signal?: AbortSignal): Promise<{
|
|
59
|
+
status: 'unchanged' | 'changed';
|
|
60
|
+
observedHash: string | null;
|
|
61
|
+
}>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const CONFIRMATION_OF = Object.freeze({ latest: 'sequencer-confirmed' });
|
|
2
|
+
export const CONFIRMATION_NOTES = Object.freeze({
|
|
3
|
+
finalized: 'Recorded before the instant-read rule: the snapshot was the configured provider\'s finalized tag, which is not independent consensus verification.',
|
|
4
|
+
safe: 'Recorded before the instant-read rule: the snapshot was the provider\'s safe block (batch posted to the parent chain); it was not finalized.',
|
|
5
|
+
'sequencer-confirmed': 'Snapshot is the latest block (sequencer-confirmed) when the run started. It is not finalized: a reorg can still replace it. Every state read names this block, and its hash is rechecked at completion; a changed hash is reported, never hidden.',
|
|
6
|
+
});
|
|
7
|
+
const INSTANT = 'Owner rule: reads never wait for finality. The latest block is pinned for the run and its hash is rechecked at completion.';
|
|
8
|
+
const policy = (chain, chainId, pinBy, basis) => Object.freeze({ chain, chainId, tags: Object.freeze(['latest']), pinBy, confirmationOf: CONFIRMATION_OF, basis });
|
|
9
|
+
const ROBINHOOD = `${INSTANT} Robinhood Chain state reads name the pinned block hash (EIP-1898); the official RPC served hash-pinned state on 2026-09-14.`;
|
|
10
|
+
export const READ_SNAPSHOT_POLICIES = Object.freeze({
|
|
11
|
+
ethereum: policy('ethereum', 1, 'number', INSTANT),
|
|
12
|
+
base: policy('base', 8453, 'number', INSTANT),
|
|
13
|
+
robinhood: policy('robinhood', 4663, 'hash', ROBINHOOD),
|
|
14
|
+
'robinhood-testnet': policy('robinhood-testnet', 46630, 'hash', ROBINHOOD),
|
|
15
|
+
});
|
|
16
|
+
export class ReadSnapshotError extends Error {
|
|
17
|
+
category;
|
|
18
|
+
constructor(category, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.category = category;
|
|
21
|
+
this.name = 'ReadSnapshotError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function readSnapshotPolicy(chain) {
|
|
25
|
+
const policy = READ_SNAPSHOT_POLICIES[chain];
|
|
26
|
+
if (!policy)
|
|
27
|
+
throw new ReadSnapshotError('unsupported', `No read-snapshot policy is registered for ${chain}.`);
|
|
28
|
+
return policy;
|
|
29
|
+
}
|
|
30
|
+
/** The block parameter for state reads: the pinned block number, or the pinned canonical block hash on hash-pinned chains. */
|
|
31
|
+
export function snapshotBlockTag(block) {
|
|
32
|
+
return block.pinBy === 'hash' ? { blockHash: block.blockHash, requireCanonical: true } : `0x${BigInt(block.blockNumber).toString(16)}`;
|
|
33
|
+
}
|
|
34
|
+
const quantity = (value) => { if (typeof value !== 'string' || !/^0x[0-9a-fA-F]{1,64}$/.test(value))
|
|
35
|
+
throw new ReadSnapshotError('invalid-response', 'Expected a hex quantity.'); return BigInt(value); };
|
|
36
|
+
function parseHeader(value) {
|
|
37
|
+
const block = value;
|
|
38
|
+
if (!block || typeof block.hash !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(block.hash))
|
|
39
|
+
throw new ReadSnapshotError('invalid-response', 'Malformed block header.');
|
|
40
|
+
return { blockNumber: quantity(block.number).toString(), blockHash: block.hash.toLowerCase(), timestamp: quantity(block.timestamp).toString() };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Chain-read helper for services outside the tool recorder: verifies chain identity, pins the latest block under the chain's policy
|
|
44
|
+
* and returns the block tag every later read must use.
|
|
45
|
+
*/
|
|
46
|
+
export async function pinReadSnapshot(chain, transport, signal = AbortSignal.timeout(20000)) {
|
|
47
|
+
const policy = readSnapshotPolicy(chain);
|
|
48
|
+
if (quantity(await transport.request('eth_chainId', [], signal)) !== BigInt(policy.chainId))
|
|
49
|
+
throw new ReadSnapshotError('chain-mismatch', `The RPC is not ${chain}.`);
|
|
50
|
+
for (const [index, tag] of policy.tags.entries()) {
|
|
51
|
+
const last = index === policy.tags.length - 1;
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await transport.request('eth_getBlockByNumber', [tag, false], signal);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (last || signal.aborted)
|
|
58
|
+
throw error;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (result === null) {
|
|
62
|
+
if (last)
|
|
63
|
+
throw new ReadSnapshotError('provider-unavailable', `The RPC returned no ${tag} block.`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const block = parseHeader(result);
|
|
67
|
+
const confirmation = policy.confirmationOf[tag];
|
|
68
|
+
return { chain, chainId: policy.chainId, ...block, tag, confirmation, note: CONFIRMATION_NOTES[confirmation], blockTag: snapshotBlockTag({ ...block, pinBy: policy.pinBy }) };
|
|
69
|
+
}
|
|
70
|
+
throw new ReadSnapshotError('provider-unavailable', 'No snapshot tag was served.');
|
|
71
|
+
}
|
|
72
|
+
/** Rechecks that the pinned hash is still canonical at its height. `changed` means every read made at the snapshot must be discarded. */
|
|
73
|
+
export async function recheckReadSnapshot(pinned, transport, signal = AbortSignal.timeout(20000)) {
|
|
74
|
+
const result = await transport.request('eth_getBlockByNumber', [`0x${BigInt(pinned.blockNumber).toString(16)}`, false], signal);
|
|
75
|
+
const observedHash = result === null ? null : parseHeader(result).blockHash;
|
|
76
|
+
return { status: observedHash === pinned.blockHash ? 'unchanged' : 'changed', observedHash };
|
|
77
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type AgentManifest, type ResearchInput, type TokenResearchReport } from '../../contracts/src/index.js';
|
|
2
|
+
export declare const EVALUATION_SUITE_VERSION = "token-researcher-local-fixtures-v1";
|
|
3
|
+
export declare const EVALUATION_SUITE_HASH: string;
|
|
4
|
+
/**
|
|
5
|
+
* Token Researcher 0.3.0 (market policy) suite. v1 above is unchanged, so every earlier evaluation keeps its hash. A version with `marketPolicy`
|
|
6
|
+
* runs the v1 cases plus market cases, and the report's market section must equal its derivation from the captured receipts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MARKET_EVALUATION_SUITE_VERSION = "token-researcher-local-fixtures-v2";
|
|
9
|
+
export declare const MARKET_EVALUATION_SUITE_HASH: string;
|
|
10
|
+
/** Suite version and hash for a Token Researcher manifest: v2 when it declares a market policy, otherwise the unchanged v1. */
|
|
11
|
+
export declare const tokenResearchSuiteFor: (manifest: {
|
|
12
|
+
marketPolicy?: unknown;
|
|
13
|
+
}) => {
|
|
14
|
+
version: string;
|
|
15
|
+
hash: string;
|
|
16
|
+
};
|
|
17
|
+
export interface FixtureResult {
|
|
18
|
+
id: string;
|
|
19
|
+
chain: string;
|
|
20
|
+
passed: boolean;
|
|
21
|
+
issues: string[];
|
|
22
|
+
report: TokenResearchReport | null;
|
|
23
|
+
reportHash: string | null;
|
|
24
|
+
transportCalls: number;
|
|
25
|
+
}
|
|
26
|
+
export interface EvaluationSnapshot {
|
|
27
|
+
kind: 'deterministic_fixture_evaluation';
|
|
28
|
+
visibility: 'local_only';
|
|
29
|
+
suiteVersion: string;
|
|
30
|
+
suiteHash: string;
|
|
31
|
+
manifestHash: string;
|
|
32
|
+
status: 'passed' | 'failed';
|
|
33
|
+
summary: {
|
|
34
|
+
passed: number;
|
|
35
|
+
total: number;
|
|
36
|
+
issues: string[];
|
|
37
|
+
};
|
|
38
|
+
fixtures: FixtureResult[];
|
|
39
|
+
limitations: string[];
|
|
40
|
+
}
|
|
41
|
+
/** This invariant is shared by local review and fixtures; callers still must verify the stored run/checkpoint binding. */
|
|
42
|
+
export declare function canonicalMetadataObserved(report: TokenResearchReport): boolean;
|
|
43
|
+
export declare function validateReportBinding(raw: unknown, manifest: AgentManifest, input: ResearchInput): TokenResearchReport;
|
|
44
|
+
/** No creator-supplied fixture, executor, URL, time, or expected output is accepted. */
|
|
45
|
+
export declare function evaluateManifest(manifest: AgentManifest): Promise<EvaluationSnapshot>;
|