otto-intel-mcp 0.1.2 → 0.1.3

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 (51) hide show
  1. package/README.md +4 -2
  2. package/dist/adapter/cdp-signer.d.ts +44 -5
  3. package/dist/adapter/cdp-signer.js +168 -18
  4. package/dist/adapter/evm-call-failure.d.ts +29 -0
  5. package/dist/adapter/evm-call-failure.js +109 -0
  6. package/dist/adapter/refusal.d.ts +1 -1
  7. package/dist/adapter/refusal.js +13 -0
  8. package/dist/adapter/route-simulator.d.ts +91 -0
  9. package/dist/adapter/route-simulator.js +143 -0
  10. package/dist/executable-equities.d.ts +55 -0
  11. package/dist/executable-equities.js +116 -0
  12. package/dist/executable-yield-markets.d.ts +261 -0
  13. package/dist/executable-yield-markets.js +154 -0
  14. package/dist/execution-delegated-definition.d.ts +1 -0
  15. package/dist/execution-delegated-definition.js +13 -2
  16. package/dist/execution-delegation-admin-definition.d.ts +126 -5
  17. package/dist/execution-delegation-admin-definition.js +154 -5
  18. package/dist/execution-delegation-admin.d.ts +48 -2
  19. package/dist/execution-delegation-admin.js +314 -12
  20. package/dist/execution-delegation-cap-store.d.ts +369 -0
  21. package/dist/execution-delegation-cap-store.js +1083 -0
  22. package/dist/execution-delegation-policy.d.ts +153 -6
  23. package/dist/execution-delegation-policy.js +260 -16
  24. package/dist/execution-delegation.d.ts +35 -0
  25. package/dist/execution-delegation.js +224 -9
  26. package/dist/execution-errors.d.ts +1 -1
  27. package/dist/execution-errors.js +98 -0
  28. package/dist/execution-index.d.ts +3 -2
  29. package/dist/execution-index.js +3 -2
  30. package/dist/execution-registration.d.ts +51 -2
  31. package/dist/execution-registration.js +45 -2
  32. package/dist/execution-tools.d.ts +44 -1
  33. package/dist/execution-tools.js +157 -13
  34. package/dist/lifi-execution-client.d.ts +6 -0
  35. package/dist/lifi-execution-client.js +3 -0
  36. package/dist/stock-buy-definition.d.ts +338 -0
  37. package/dist/stock-buy-definition.js +127 -0
  38. package/dist/stock-buy-oracle.d.ts +8 -0
  39. package/dist/stock-buy-oracle.js +22 -0
  40. package/dist/stock-buy.d.ts +17 -0
  41. package/dist/stock-buy.js +164 -0
  42. package/dist/tool-definitions.js +1 -1
  43. package/dist/yield-deposit-definition.d.ts +323 -0
  44. package/dist/yield-deposit-definition.js +121 -0
  45. package/dist/yield-deposit.d.ts +16 -0
  46. package/dist/yield-deposit.js +141 -0
  47. package/dist/yield-withdraw-definition.d.ts +306 -0
  48. package/dist/yield-withdraw-definition.js +88 -0
  49. package/dist/yield-withdraw.d.ts +21 -0
  50. package/dist/yield-withdraw.js +120 -0
  51. package/package.json +28 -4
@@ -1,9 +1,13 @@
1
1
  import { type ExecutionServiceConfig } from './execution-config.js';
2
2
  import { type DelegationServices } from './execution-delegation.js';
3
+ import { type RouteSimulator } from './adapter/route-simulator.js';
4
+ import { type PrepareSwapInput } from './execution-tool-definitions.js';
3
5
  import { type BridgeExecutionEnvelope, type ExecutionEnvelope, type HyperliquidExecutionEnvelope, type PolymarketExecutionEnvelope } from './execution-types.js';
4
6
  import { type HyperliquidInfoClient } from './hyperliquid-info-client.js';
5
7
  import { type UnsignedHyperliquidAction, type UnsignedHyperliquidActionRequest } from './hyperliquid-order-builder.js';
6
- import { type LiFiQuoteClient } from './lifi-execution-client.js';
8
+ import { type LiFiQuote, type LiFiQuoteClient } from './lifi-execution-client.js';
9
+ import type { YieldVaultReader } from './yield-deposit-definition.js';
10
+ import type { EquityOracleReader } from './stock-buy-definition.js';
7
11
  import { type PolymarketClobClient } from './polymarket-clob-client.js';
8
12
  import { type UnsignedPolymarketOrder, type UnsignedPolymarketOrderRequest } from './polymarket-order-builder.js';
9
13
  /** The order-construction seam: request in, unsigned SDK-built order out. */
@@ -37,8 +41,27 @@ export interface ExecutionToolRuntime {
37
41
  * this process prepared (`prepared-artifacts.ts`).
38
42
  */
39
43
  readonly delegation?: DelegationServices;
44
+ /**
45
+ * Proves a quoted swap's calldata executes from the signing account BEFORE it is prepared
46
+ * (`adapter/route-simulator.ts`). Present on a server booted with a Base RPC (the delegated rail's);
47
+ * absent elsewhere, in which case the envelope records `route_simulated:false`.
48
+ */
49
+ readonly routeSimulator?: RouteSimulator;
50
+ /**
51
+ * The one on-chain read `otto_prepare_yield_deposit` needs (the vault's own `previewDeposit`), present
52
+ * on the hosted service over the delegation rig's Base public client; injectable so the parity guard is
53
+ * mutation-provable. Absent → the yield constructor refuses by name, nothing is prepared.
54
+ */
55
+ readonly yieldVaultReader?: YieldVaultReader;
56
+ /**
57
+ * The one on-chain read `otto_prepare_stock_buy` needs (the equity's Chainlink mark), present on the
58
+ * hosted service over the same Base public client; injectable so the parity guard is mutation-provable.
59
+ * Absent → the stock constructor refuses by name, nothing is prepared.
60
+ */
61
+ readonly equityOracleReader?: EquityOracleReader;
40
62
  }
41
63
  export interface ExecutionRuntimeOverrides {
64
+ readonly routeSimulator?: RouteSimulator;
42
65
  readonly quoteClient?: LiFiQuoteClient;
43
66
  readonly clobClient?: PolymarketClobClient;
44
67
  readonly polymarketOrderBuilder?: PolymarketOrderBuilder;
@@ -48,6 +71,8 @@ export interface ExecutionRuntimeOverrides {
48
71
  readonly entropy?: () => Uint8Array;
49
72
  readonly clientOrderIdEntropy?: () => Uint8Array;
50
73
  readonly delegation?: DelegationServices;
74
+ readonly yieldVaultReader?: YieldVaultReader;
75
+ readonly equityOracleReader?: EquityOracleReader;
51
76
  }
52
77
  export declare function createExecutionToolRuntime(config: ExecutionServiceConfig, overrides?: ExecutionRuntimeOverrides): ExecutionToolRuntime;
53
78
  export declare function createExecutionToolRuntimeFromEnv(env?: NodeJS.ProcessEnv): ExecutionToolRuntime | undefined;
@@ -58,7 +83,25 @@ export declare function createExecutionToolRuntimeFromEnv(env?: NodeJS.ProcessEn
58
83
  * the tool.
59
84
  */
60
85
  export declare function createExecutionToolRuntimeWithDelegationFromEnv(env?: NodeJS.ProcessEnv): Promise<ExecutionToolRuntime | undefined>;
86
+ /** How many LI.FI tools the constructor will try before it refuses: the first quote plus two re-quotes. */
87
+ export declare const MAX_ROUTE_ATTEMPTS: 3;
61
88
  export declare function prepareSwap(runtime: ExecutionToolRuntime, rawInput: unknown, signal?: AbortSignal): Promise<ExecutionEnvelope>;
89
+ /**
90
+ * The route-set and fee pre-checks, then ONE LI.FI quote for the parsed swap input. Split from
91
+ * `prepareSwapFromQuote` so a constructor that must read the quote itself (the Yield Copilot deposit:
92
+ * expected shares, the router's floor, the fee lines) quotes exactly once and builds the envelope from
93
+ * that same quote — a second quote could route through a different tool and carry a different floor.
94
+ */
95
+ export declare function quoteSwap(runtime: ExecutionToolRuntime, input: PrepareSwapInput, signal?: AbortSignal): Promise<ExecutableSwapQuote>;
96
+ /** A LI.FI quote as `quoteSwap` returns it: the quote plus the route facts the envelope records. */
97
+ export type ExecutableSwapQuote = LiFiQuote & {
98
+ readonly route: {
99
+ readonly tool: string;
100
+ readonly simulated: boolean;
101
+ };
102
+ };
103
+ /** Decode, assert and commit the envelope for a parsed swap input over the quote `quoteSwap` returned for it. */
104
+ export declare function prepareSwapFromQuote(runtime: ExecutionToolRuntime, input: PrepareSwapInput, quote: LiFiQuote | ExecutableSwapQuote): ExecutionEnvelope;
62
105
  /**
63
106
  * Build the ordered source-chain plan for a cross-chain native-USDC bridge over the enumerated v1
64
107
  * Circle-CCTP route set. Prepare-only: the plan is handed to the caller's own custody to sign.
@@ -4,6 +4,7 @@ import { attributedFeeAmount, HL_BUILDER_ATTRIBUTION_STATUS, HYPERLIQUID_L1_SIGN
4
4
  import { refuse } from './execution-errors.js';
5
5
  import { createDelegationServicesFromEnv, } from './execution-delegation.js';
6
6
  import { recordPreparedArtifact } from './prepared-artifacts.js';
7
+ import { createRouteSimulator } from './adapter/route-simulator.js';
7
8
  import { swapIntentDigestInput } from './execution-intent.js';
8
9
  import { BRIDGE_EXECUTION_ENVELOPE_SCHEMA, EXECUTION_ENVELOPE_SCHEMA, HYPERLIQUID_EXECUTION_ENVELOPE_SCHEMA, POLYMARKET_EXECUTION_ENVELOPE_SCHEMA, PREPARE_BRIDGE_INPUT_SCHEMA, PREPARE_PERP_ORDER_INPUT_SCHEMA, PREPARE_POLYMARKET_ORDER_INPUT_SCHEMA, PREPARE_SWAP_INPUT_SCHEMA, } from './execution-tool-definitions.js';
9
10
  import { accountBinding, artifactCommitmentDigest, bridgeAccountBinding, createArtifactId, executionDigest, hyperliquidAccountBinding, polymarketAccountBinding, } from './execution-types.js';
@@ -11,8 +12,12 @@ import { RestHyperliquidInfoClient, } from './hyperliquid-info-client.js';
11
12
  import { assertHyperliquidBracketCoherent, assertHyperliquidPriceOnGrid, assertHyperliquidSizeOnGrid, decodeAndAssertHyperliquidAction, hyperliquidSigningDeclaration, } from './hyperliquid-order-assertions.js';
12
13
  import { buildUnsignedHyperliquidOrderAction, } from './hyperliquid-order-builder.js';
13
14
  import { decodeAndAssertLiFiBridge } from './lifi-bridge-assertions.js';
14
- import { RestLiFiQuoteClient } from './lifi-execution-client.js';
15
+ import { RestLiFiQuoteClient, } from './lifi-execution-client.js';
15
16
  import { decodeAndAssertLiFiSwap } from './lifi-swap-assertions.js';
17
+ import { modelBWithdrawToken } from './execution-delegation-policy.js';
18
+ import { viemYieldVaultReader } from './yield-withdraw.js';
19
+ import { viemEquityOracleReader } from './stock-buy-oracle.js';
20
+ import { EXECUTABLE_EQUITY_TOKEN_SET } from './executable-equities.js';
16
21
  import { RestPolymarketClobClient } from './polymarket-clob-client.js';
17
22
  import { assertPolymarketBuilderAttributionRuled, assertPolymarketPriceOnTick, decodeAndAssertPolymarketOrder, polymarketAtomicUnits, } from './polymarket-order-assertions.js';
18
23
  import { buildUnsignedPolymarketOrder, } from './polymarket-order-builder.js';
@@ -31,6 +36,9 @@ export function createExecutionToolRuntime(config, overrides = {}) {
31
36
  entropy: overrides.entropy ?? (() => randomBytes(10)),
32
37
  clientOrderIdEntropy: overrides.clientOrderIdEntropy ?? (() => randomBytes(16)),
33
38
  ...(overrides.delegation ? { delegation: overrides.delegation } : {}),
39
+ ...(overrides.routeSimulator ? { routeSimulator: overrides.routeSimulator } : {}),
40
+ ...(overrides.yieldVaultReader ? { yieldVaultReader: overrides.yieldVaultReader } : {}),
41
+ ...(overrides.equityOracleReader ? { equityOracleReader: overrides.equityOracleReader } : {}),
34
42
  });
35
43
  }
36
44
  export function createExecutionToolRuntimeFromEnv(env = process.env) {
@@ -52,8 +60,21 @@ export async function createExecutionToolRuntimeWithDelegationFromEnv(env = proc
52
60
  }
53
61
  return undefined;
54
62
  }
55
- return createExecutionToolRuntime(config, delegation ? { delegation } : {});
63
+ // The delegated rail's Base client doubles as the route simulator's view of the chain (#2125) and as
64
+ // the yield constructor's vault read and the stock constructor's Chainlink read (#2129).
65
+ return createExecutionToolRuntime(config, {
66
+ ...(delegation
67
+ ? {
68
+ delegation,
69
+ yieldVaultReader: viemYieldVaultReader(delegation.rig.rig.pub),
70
+ equityOracleReader: viemEquityOracleReader(delegation.rig.rig.pub),
71
+ }
72
+ : {}),
73
+ ...(delegation?.pub ? { routeSimulator: createRouteSimulator(delegation.pub) } : {}),
74
+ });
56
75
  }
76
+ /** How many LI.FI tools the constructor will try before it refuses: the first quote plus two re-quotes. */
77
+ export const MAX_ROUTE_ATTEMPTS = 3;
57
78
  function normalizedAddress(value) {
58
79
  return getAddress(value.toLowerCase());
59
80
  }
@@ -135,6 +156,29 @@ function capabilityFor(profile) {
135
156
  }
136
157
  export async function prepareSwap(runtime, rawInput, signal) {
137
158
  const input = PREPARE_SWAP_INPUT_SCHEMA.parse(rawInput);
159
+ /**
160
+ * 🔴 REGISTRY VAULT SHARES ARE EXCLUSIVE TO THE YIELD CONSTRUCTORS (codex gate r1 HIGH 2). A generic swap
161
+ * whose input or output is a Yield Copilot vault share would reach the delegated gate as an
162
+ * indistinguishable `operation: 'swap'` artifact and be admitted as a deposit/withdraw WITHOUT the registry
163
+ * commitment and the vault-parity guard the yield constructors run. So this public constructor refuses that
164
+ * token class by name; `otto_prepare_yield_deposit` / `otto_prepare_yield_withdraw` are the only paths.
165
+ */
166
+ for (const token of [input.fromToken, input.toToken]) {
167
+ if (modelBWithdrawToken(normalizedAddress(token))) {
168
+ return refuse('UNSUPPORTED_LIFI_ROUTE', `${token} is a Yield Copilot vault share token; use otto_prepare_yield_deposit / otto_prepare_yield_withdraw, which pin the registry row and check vault parity`);
169
+ }
170
+ // Same discipline for the executable equities (2026-09-03): a generic swap into a B20 token would
171
+ // reach the delegated gate as an indistinguishable swap artifact WITHOUT the registry commitment,
172
+ // the Chainlink price-parity guard and the freshness gate. The stock constructor is the only path.
173
+ if (EXECUTABLE_EQUITY_TOKEN_SET.has(token.toLowerCase())) {
174
+ return refuse('UNSUPPORTED_LIFI_ROUTE', `${token} is an executable tokenized equity; use otto_prepare_stock_buy, which pins the registry row and checks price parity against the equity's own Chainlink mark`);
175
+ }
176
+ }
177
+ const quote = await quoteSwap(runtime, input, signal);
178
+ return prepareSwapFromQuote(runtime, input, quote);
179
+ }
180
+ /** The swap's account profile, normalized; refuses a profile whose chain is not the swap's chain. */
181
+ function swapProfile(input) {
138
182
  const profile = {
139
183
  kind: input.accountProfile.kind,
140
184
  address: normalizedAddress(input.accountProfile.address),
@@ -143,24 +187,114 @@ export async function prepareSwap(runtime, rawInput, signal) {
143
187
  if (profile.chainId !== input.chainId) {
144
188
  return refuse('ACCOUNT_BINDING_MISMATCH', 'account profile chain does not match swap chain');
145
189
  }
190
+ return profile;
191
+ }
192
+ /**
193
+ * The route-set and fee pre-checks, then ONE LI.FI quote for the parsed swap input. Split from
194
+ * `prepareSwapFromQuote` so a constructor that must read the quote itself (the Yield Copilot deposit:
195
+ * expected shares, the router's floor, the fee lines) quotes exactly once and builds the envelope from
196
+ * that same quote — a second quote could route through a different tool and carry a different floor.
197
+ */
198
+ export async function quoteSwap(runtime, input, signal) {
199
+ const profile = swapProfile(input);
146
200
  if (!reviewedLiFiFeeForwarders(input.chainId)) {
147
201
  return refuse('UNSUPPORTED_LIFI_ROUTE', 'chain is outside the reviewed LI.FI v1 route set');
148
202
  }
149
- const fromToken = normalizedAddress(input.fromToken);
150
- const toToken = normalizedAddress(input.toToken);
151
203
  const fromAmount = BigInt(input.fromAmount);
152
204
  if (attributedFeeAmount(fromAmount, runtime.config.lifiFee) <= 0n) {
153
205
  return refuse('UNSUPPORTED_LIFI_ROUTE', 'input amount is too small for the configured Otto fee');
154
206
  }
155
- const quote = await runtime.quoteClient.getSameChainSwapQuote({
156
- chainId: input.chainId,
157
- fromToken,
158
- toToken,
159
- fromAmount: input.fromAmount,
160
- sender: profile.address,
161
- receiver: profile.address,
162
- maxSlippageBps: input.slippage.maxBps,
163
- }, signal);
207
+ /**
208
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
209
+ * 🔴 A ROUTE IS QUOTED ONLY ONCE IT HAS BEEN SEEN TO EXECUTE (2026-09-03, the first live delegated swap)
210
+ * ══════════════════════════════════════════════════════════════════════════════════════════════
211
+ * LI.FI picks the exchange tool per quote. That day it picked KyberSwap for a $1 USDC→WETH swap, and
212
+ * the diamond call reverted deterministically — at any gas, on a fresh quote too — while the same
213
+ * request through SushiSwap executed. Every assertion the constructor makes is about SHAPE (selector,
214
+ * receiver, fee, minimum output); none of them can see that a tool's calldata fails inside the diamond.
215
+ *
216
+ * So, on a server that has a chain view (the delegated rail's Base client), the quoted swap step is
217
+ * SIMULATED from the executing account with its preconditions overridden (balance and allowance to the
218
+ * spender — neither exists at prepare time). A route that reverts is re-quoted with that tool DENIED,
219
+ * bounded to `MAX_ROUTE_ATTEMPTS` tools; if none executes the quote refuses and names them. A
220
+ * simulation that cannot be run (no layout for the token, a node that ignores overrides) refuses too —
221
+ * an unproven route is not prepared on a server that was supposed to prove it.
222
+ *
223
+ * It lives HERE, in the one quote every swap-shaped constructor takes — the public swap and the Yield
224
+ * Copilot deposit/withdraw alike — so the quote a caller reads (expected shares, the floor, the fee
225
+ * lines) IS the quote that was proven, and `prepareSwapFromQuote` builds the envelope from exactly it.
226
+ * The proven tool and whether it was simulated ride on the returned quote (`route`), are recorded in
227
+ * the envelope's assertions, and the delegated receipt repeats them, so the receipt says which venue
228
+ * executed. A server without a chain view (the prepare-only x402 constructor) quotes as before and
229
+ * records `route_simulated:false`.
230
+ */
231
+ const fromToken = normalizedAddress(input.fromToken);
232
+ const toToken = normalizedAddress(input.toToken);
233
+ const denied = [];
234
+ for (let attempt = 1; attempt <= MAX_ROUTE_ATTEMPTS; attempt += 1) {
235
+ const quote = await runtime.quoteClient.getSameChainSwapQuote({
236
+ chainId: input.chainId,
237
+ fromToken,
238
+ toToken,
239
+ fromAmount: input.fromAmount,
240
+ sender: profile.address,
241
+ receiver: profile.address,
242
+ maxSlippageBps: input.slippage.maxBps,
243
+ // A COPY: the quote client must never see this list grow after it answered.
244
+ ...(denied.length > 0 ? { denyExchanges: Object.freeze([...denied]) } : {}),
245
+ }, signal);
246
+ const routeTool = typeof quote.tool === 'string' && quote.tool.trim() ? quote.tool.trim().slice(0, 48) : 'unknown';
247
+ if (!runtime.routeSimulator) {
248
+ return Object.freeze({ ...quote, route: Object.freeze({ tool: routeTool, simulated: false }) });
249
+ }
250
+ /**
251
+ * 🔴 SHAPE BEFORE SIMULATION (merge gate 2026-09-04). The quote is decoded and asserted HERE, on the
252
+ * quote-stage input, before the node sees it: a quote whose spender or target is not the diamond, whose
253
+ * selector, receiver, fee or floor is wrong, is refused by the assertion — it is never simulated, and a
254
+ * simulation that failed only because the quote's own `approvalAddress` was wrong can never deny a
255
+ * healthy tool. What the node simulates is the ASSERTED transaction and spender, the same bytes
256
+ * `prepareSwapFromQuote` asserts again (with the caller's floor) when it builds the envelope.
257
+ */
258
+ const asserted = decodeAndAssertLiFiSwap({
259
+ quote,
260
+ profile,
261
+ chainId: input.chainId,
262
+ fromToken,
263
+ toToken,
264
+ fromAmount,
265
+ maxSlippageBps: input.slippage.maxBps,
266
+ minAmountOut: input.slippage.minAmountOut ? BigInt(input.slippage.minAmountOut) : undefined,
267
+ config: runtime.config,
268
+ });
269
+ const simulation = await runtime.routeSimulator.simulate({
270
+ from: profile.address,
271
+ to: asserted.transaction.to,
272
+ data: asserted.transaction.data,
273
+ value: BigInt(asserted.transaction.value),
274
+ token: fromToken,
275
+ spender: asserted.approvalSpender,
276
+ amount: fromAmount,
277
+ });
278
+ if (simulation.outcome === 'ok') {
279
+ return Object.freeze({ ...quote, route: Object.freeze({ tool: routeTool, simulated: true }) });
280
+ }
281
+ if (simulation.outcome === 'unavailable') {
282
+ return refuse('UNSUPPORTED_LIFI_ROUTE', `[ROUTE_SIMULATION_UNAVAILABLE] the quoted route could not be simulated from ${profile.address}, so it is not prepared: ${simulation.detail}`);
283
+ }
284
+ denied.push(routeTool);
285
+ // A tool LI.FI did not name cannot be denied on the next quote; re-asking would only repeat it.
286
+ if (attempt === MAX_ROUTE_ATTEMPTS || routeTool === 'unknown') {
287
+ return refuse('UNSUPPORTED_LIFI_ROUTE', `[ROUTE_SIMULATION_REVERTED] no LI.FI route executes from ${profile.address} for this swap: tried ${denied.join(', ')} (last: ${simulation.detail})`);
288
+ }
289
+ }
290
+ return refuse('UNSUPPORTED_LIFI_ROUTE', 'no LI.FI quote was obtained');
291
+ }
292
+ /** Decode, assert and commit the envelope for a parsed swap input over the quote `quoteSwap` returned for it. */
293
+ export function prepareSwapFromQuote(runtime, input, quote) {
294
+ const profile = swapProfile(input);
295
+ const fromToken = normalizedAddress(input.fromToken);
296
+ const toToken = normalizedAddress(input.toToken);
297
+ const fromAmount = BigInt(input.fromAmount);
164
298
  const asserted = decodeAndAssertLiFiSwap({
165
299
  quote,
166
300
  profile,
@@ -172,6 +306,13 @@ export async function prepareSwap(runtime, rawInput, signal) {
172
306
  minAmountOut: input.slippage.minAmountOut ? BigInt(input.slippage.minAmountOut) : undefined,
173
307
  config: runtime.config,
174
308
  });
309
+ // The route facts `quoteSwap` proved; a bare quote (a caller that quoted elsewhere) was not simulated.
310
+ const route = quote.route ?? {
311
+ tool: typeof quote.tool === 'string' && quote.tool.trim() ? quote.tool.trim().slice(0, 48) : 'unknown',
312
+ simulated: false,
313
+ };
314
+ const routeTool = route.tool;
315
+ const routeSimulated = route.simulated;
175
316
  const capability = capabilityFor(profile);
176
317
  const approvalCalldata = (amount) => encodeFunctionData({
177
318
  abi: ERC20_APPROVE_ABI,
@@ -230,6 +371,9 @@ export async function prepareSwap(runtime, rawInput, signal) {
230
371
  },
231
372
  assertions: [
232
373
  ...asserted.assertions,
374
+ // The route that was prepared, and whether it was proven to execute — repeated on the delegated receipt.
375
+ { id: `route_tool:${routeTool}`, result: 'passed' },
376
+ { id: `route_simulated:${routeSimulated}`, result: 'passed' },
233
377
  { id: 'payload_digest_committed', result: 'passed' },
234
378
  { id: 'erc20_allowance_reset_ordered', result: 'passed' },
235
379
  { id: 'artifact_expiry_committed', result: 'passed' },
@@ -39,6 +39,12 @@ export interface SameChainSwapQuoteRequest {
39
39
  readonly sender: string;
40
40
  readonly receiver: string;
41
41
  readonly maxSlippageBps: number;
42
+ /**
43
+ * LI.FI exchange tools the quote must NOT route through. Set by the constructor when a tool's
44
+ * calldata failed the prepare-time simulation (2026-09-03: KyberSwap reverted from the diamond while
45
+ * SushiSwap executed); never a caller seam.
46
+ */
47
+ readonly denyExchanges?: readonly string[];
42
48
  }
43
49
  export interface CrossChainBridgeQuoteRequest {
44
50
  readonly fromChainId: number;
@@ -65,6 +65,9 @@ export function buildLiFiQuoteUrl(request, config) {
65
65
  ];
66
66
  for (const [name, value] of semanticFields)
67
67
  url.searchParams.append(name, value);
68
+ if (request.denyExchanges && request.denyExchanges.length > 0) {
69
+ url.searchParams.append('denyExchanges', request.denyExchanges.join(','));
70
+ }
68
71
  url.searchParams.append('integrator', LIFI_INTEGRATOR);
69
72
  url.searchParams.append('fee', config.lifiFee.decimal);
70
73
  return url;