Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ node_modules
# IDE - Cursor
.cursor/*

# Nx local cache (never commit)
.nx/

# misc
/.sass-cache
/connect.lock
Expand Down
32 changes: 30 additions & 2 deletions apps/agentic-server/src/tools/initiateSwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ async function executeSwapInternal({

const { sellAsset, buyAsset } = await resolveSwapAssets(sellAssetInput, buyAssetInput, walletContext)

// Guard likely USD-vs-token amount mismatches for expensive assets.
// Example mistake: entering "100" for ETH when intent was "$100 worth of ETH".
const sellAssetPrice = parseFloat(sellAsset.price || '0')
const sellAmountNum = parseFloat(sellAmountCrypto)
const sellValueUsd = sellAssetPrice > 0 ? sellAmountNum * sellAssetPrice : 0
const hasCurrencyLikePrecision = /^\d+(\.\d{1,2})?$/.test(sellAmountCrypto.trim())
const looksLikeUsdAsTokenAmount =
hasCurrencyLikePrecision && sellAssetPrice >= 10 && sellValueUsd >= 50_000 && sellAmountNum <= 100_000

const sellAddress = getAddressForChain(walletContext, sellAsset.chainId)
const buyAddress = getAddressForChain(walletContext, buyAsset.chainId)

Expand All @@ -230,7 +239,18 @@ async function executeSwapInternal({

const needsApproval = allowanceData.isApprovalRequired

await validateSufficientBalance(sellAddress, sellAsset, sellAmountCrypto)
try {
await validateSufficientBalance(sellAddress, sellAsset, sellAmountCrypto)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (looksLikeUsdAsTokenAmount && message.includes('Insufficient')) {
throw new Error(
`${message} This request may be using a USD amount as token units. ` +
`If you meant a dollar value, use the USD swap flow (e.g. "$${sellAmountCrypto} worth").`
)
}
throw error
}

const approvalTx = buildApprovalTransaction(
needsApproval,
Expand Down Expand Up @@ -274,7 +294,15 @@ async function executeSwapInternal({
export const initiateSwapSchema = z.object({
sellAsset: assetInputSchema.describe('Asset to sell'),
buyAsset: assetInputSchema.describe('Asset to buy'),
sellAmount: z.string().describe('Amount to sell in crypto tokens, e.g. 1 for 1 ETH, 0.5 for 0.5 SOL'),
sellAmount: z
.string()
.refine(val => !/^\d{15,}/.test(val.trim()), {
message:
'sellAmount looks like a base-unit value (15+ digits). Use human-readable token amounts (e.g. "1" for 1 ETH, not "1000000000000000000").',
})
.describe(
'Amount to sell in TOKEN units (not USD), e.g. "1" for 1 ETH, "0.5" for 0.5 SOL. Never pass base units (like wei), and do not pass dollar amounts here.'
),
})

export type InitiateSwapInput = z.infer<typeof initiateSwapSchema>
Expand Down
73 changes: 71 additions & 2 deletions apps/agentic-server/src/tools/limitOrder/createLimitOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { toBaseUnit } from '@shapeshiftoss/utils'
import BigNumber from 'bignumber.js'
import { z } from 'zod'

import { getSimplePrices } from '../../lib/asset/coingecko'
import { resolveCowTokenAddress } from '../../lib/composableCow'
import { COW_VAULT_RELAYER_ADDRESS, prepareCowLimitOrder } from '../../lib/cow'
import type { CowOrderSigningData } from '../../lib/cow/types'
Expand All @@ -21,13 +22,17 @@ export const createLimitOrderSchema = z.object({
network: cowSupportedNetworkSchema.describe('Network for the limit order'),
sellAmount: z
.string()
.refine(val => !/^\d{15,}/.test(val.trim()), {
message:
'sellAmount looks like a base-unit value (15+ digits). Use human-readable token amounts (e.g. "230" for 230 ARB, not "230000000000000000000").',
})
.describe(
'Amount to sell in TOKEN units, not USD (e.g., "100" for 100 USDC, "0.5" for 0.5 WETH). If the user specified a USD dollar amount, convert to token units first using getAssetPricesTool and mathCalculatorTool.'
'Amount to sell in TOKEN units, not USD (e.g., "100" for 100 USDC, "230" for 230 ARB). Never pass base units even if precision is 18 (e.g., not "230000000000000000000"). If the user specified a USD dollar amount, convert to token units first using getAssetPricesTool and mathCalculatorTool.'
),
limitPrice: z
.string()
.describe(
'How much buyAsset you receive per 1 sellAsset. "sell A when worth X B" → limitPrice=X. Example: "worth 2 USDT" → "2". For percentage-based requests ("sell when up 5%"), compute: currentPricePerToken × (1 + pct/100).'
'How much buyAsset you receive per 1 sellAsset. NEVER invert — for sub-dollar tokens (e.g. ARB at $0.50 USD selling for USDC), limitPrice ≈ 0.50, NOT 2. "sell A when worth X B" → limitPrice=X. Example: "worth 2 USDT" → "2". For percentage-based requests ("sell when up 5%"), compute: currentPricePerToken × (1 + pct/100). Use getAssetPrices if uncertain.'
),
expirationHours: z
.number()
Expand Down Expand Up @@ -93,6 +98,54 @@ export async function executeCreateLimitOrder(
resolveAsset({ symbolOrName: input.buyAsset, network: input.network }, walletContext),
])

const limitPriceNum = Number(input.limitPrice)
if (!Number.isFinite(limitPriceNum) || limitPriceNum <= 0) {
throw new Error(`Invalid limitPrice "${input.limitPrice}". It must be a positive number.`)
}

// Sanity-check limitPrice against current market rate to catch LLM inversion/base-unit errors
const priceResults = await getSimplePrices([sellAsset.assetId, buyAsset.assetId])
const sellUsdPrice = Number(priceResults.find(p => p.assetId === sellAsset.assetId)?.price ?? '0')
const buyUsdPrice = Number(priceResults.find(p => p.assetId === buyAsset.assetId)?.price ?? '0')
if (sellUsdPrice > 0 && buyUsdPrice > 0) {
const marketLimitPrice = sellUsdPrice / buyUsdPrice
const ratio = limitPriceNum / marketLimitPrice
if (!Number.isFinite(ratio) || ratio <= 0) {
throw new Error(
`Invalid limitPrice "${input.limitPrice}" for market comparison. ` +
`Expected a positive ${buyAsset.symbol}/${sellAsset.symbol} price.`
)
}
const logRatio = Math.abs(Math.log10(ratio))
const isNearUsdPrice = (usdPrice: number) => usdPrice > 0 && Math.abs(limitPriceNum - usdPrice) / usdPrice <= 0.25

// Guard likely "USD price leaked into pair price" mistakes.
// Example: ARB->EUL should be ~0.086 EUL/ARB, but passing 1.39 (EUL USD) is >10x off.
if (logRatio > 1 && (isNearUsdPrice(sellUsdPrice) || isNearUsdPrice(buyUsdPrice))) {
throw new Error(
`limitPrice ${input.limitPrice} appears to be a USD token price, not the pair price. ` +
`Expected approximately ${marketLimitPrice.toFixed(6)} ${buyAsset.symbol}/${sellAsset.symbol} ` +
`(1 ${sellAsset.symbol} = X ${buyAsset.symbol}).`
)
}

if (logRatio > 3) {
throw new Error(
`limitPrice ${input.limitPrice} is more than 1000× from the market rate (~${marketLimitPrice.toFixed(6)} ${buyAsset.symbol}/${sellAsset.symbol}). ` +
`Did you invert the price or pass a base-unit value? For ${sellAsset.symbol} at $${sellUsdPrice} selling for ${buyAsset.symbol}, limitPrice should be ~${marketLimitPrice.toFixed(6)}.`
)
}
if (logRatio > 1) {
console.warn('[createLimitOrder] limitPrice sanity check: suspicious deviation', {
inputLimitPrice: input.limitPrice,
marketLimitPrice,
ratio,
sellAsset: sellAsset.symbol,
buyAsset: buyAsset.symbol,
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Get numeric chain ID directly from network (Zod schema guarantees valid network)
const evmChainId = NETWORK_TO_CHAIN_ID[input.network]!

Expand Down Expand Up @@ -198,4 +251,20 @@ IMPORTANT:
- For percentage-based requests ("sell when up X%"), compute limitPrice = currentPricePerToken × (1 + X/100) using getAssetPrices and the maths tool`,
inputSchema: createLimitOrderSchema,
execute: executeCreateLimitOrder,
experimental_toToolResultContent: (result: CreateLimitOrderOutput) => {
const llmSigningData: Pick<CowOrderSigningData, 'domain' | 'types' | 'primaryType'> = {
domain: result.signingData.domain,
types: result.signingData.types,
primaryType: result.signingData.primaryType,
}
const llmVisible = {
summary: result.summary,
signingData: llmSigningData,
needsApproval: result.needsApproval,
approvalTx: result.approvalTx,
approvalTarget: result.approvalTarget,
trackingUrl: result.trackingUrl,
}
return [{ type: 'text' as const, text: JSON.stringify(llmVisible) }]
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
24 changes: 23 additions & 1 deletion apps/agentic-server/src/tools/stopLoss/createStopLoss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ export const createStopLossSchema = z.object({
network: cowSupportedNetworkSchema.describe('Network for the stop-loss order'),
sellAmount: z
.string()
.refine(val => !/^\d{15,}/.test(val.trim()), {
message:
'sellAmount looks like a base-unit value (15+ digits). Use human-readable token amounts (e.g. "1" for 1 WETH, not "1000000000000000000").',
})
.describe(
'Amount to sell in TOKEN units, not USD (e.g., "1" for 1 WETH, "100000" for 100000 PEPE). No commas, dollar signs, or token symbols. If the user specified a USD dollar amount, convert to token units first using getAssetPricesTool and mathCalculatorTool.'
'Amount to sell in TOKEN units, not USD (e.g., "1" for 1 WETH, "230" for 230 ARB, "100000" for 100000 PEPE). Never pass base units even if precision is 18 (e.g., not "230000000000000000000"). No commas, dollar signs, or token symbols. If the user specified a USD dollar amount, convert to token units first using getAssetPricesTool and mathCalculatorTool.'
),
triggerPrice: z
.string()
Expand Down Expand Up @@ -318,4 +322,22 @@ IMPORTANT:
- Use the maths tool if you need to calculate trigger prices from percentages`,
inputSchema: createStopLossSchema,
execute: executeCreateStopLoss,
experimental_toToolResultContent: (result: CreateStopLossOutput) => {
const llmVisible = {
summary: result.summary,
safeTransaction: result.safeTransaction,
needsApproval: result.needsApproval,
approvalTx: result.approvalTx,
approvalTarget: result.approvalTarget,
safeAddress: result.safeAddress,
orderHash: result.orderHash,
conditionalOrderParams: result.conditionalOrderParams,
needsDeposit: result.needsDeposit,
depositTx: result.depositTx,
sellTokenAddress: result.sellTokenAddress,
buyTokenAddress: result.buyTokenAddress,
validTo: result.validTo,
}
return [{ type: 'text' as const, text: JSON.stringify(llmVisible) }]
},
}
25 changes: 24 additions & 1 deletion apps/agentic-server/src/tools/twap/createTwap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ export const createTwapSchema = z.object({
network: cowSupportedNetworkSchema.describe('Network for the TWAP/DCA order'),
totalAmount: z
.string()
.refine(val => !/^\d{15,}/.test(val.trim()), {
message:
'totalAmount looks like a base-unit value (15+ digits). Use human-readable token amounts (e.g. "1000" for 1000 USDC, not "1000000000").',
})
.describe(
'Total amount to sell in TOKEN units, not USD (e.g., "1000" for 1000 USDC, "0.5" for 0.5 WETH). If the user specified a USD dollar amount, convert to token units first using getAssetPricesTool and mathCalculatorTool.'
'Total amount to sell in TOKEN units, not USD (e.g., "1000" for 1000 USDC, "230" for 230 ARB, "0.5" for 0.5 WETH). Never pass base units even if precision is 18 (e.g., not "230000000000000000000"). If the user specified a USD dollar amount, convert to token units first using getAssetPricesTool and mathCalculatorTool.'
),
durationSeconds: z
.number()
Expand Down Expand Up @@ -271,4 +275,23 @@ IMPORTANT:
- Native tokens (ETH) must be wrapped (WETH) to sell`,
inputSchema: createTwapSchema,
execute: executeCreateTwap,
experimental_toToolResultContent: (result: CreateTwapOutput) => {
const llmVisible = {
summary: result.summary,
safeTransaction: result.safeTransaction,
needsApproval: result.needsApproval,
approvalTx: result.approvalTx,
approvalTarget: result.approvalTarget,
safeAddress: result.safeAddress,
orderHash: result.orderHash,
conditionalOrderParams: result.conditionalOrderParams,
needsDeposit: result.needsDeposit,
depositTx: result.depositTx,
sellTokenAddress: result.sellTokenAddress,
buyTokenAddress: result.buyTokenAddress,
durationSeconds: result.durationSeconds,
warnings: result.warnings,
}
return [{ type: 'text' as const, text: JSON.stringify(llmVisible) }]
},
}
Loading