Skip to content

Conversation

zdql
Copy link
Contributor

@zdql zdql commented Oct 9, 2025

Shows how easy it is to add arbitrary upstream tools to the router.

There might be a better interface... should it proxy? etc.

Working well for 402 for now

Copy link

vercel bot commented Oct 9, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
assistant-ui-template Ready Ready Preview Comment Oct 14, 2025 11:41pm
echo-control Ready Ready Preview Comment Oct 14, 2025 11:41pm
echo-next-boilerplate Ready Ready Preview Comment Oct 14, 2025 11:41pm
echo-next-image Ready Ready Preview Comment Oct 14, 2025 11:41pm
echo-next-sdk-example Ready Ready Preview Comment Oct 14, 2025 11:41pm
echo-video-template Ready Ready Preview Comment Oct 14, 2025 11:41pm
echo-vite-sdk-example Ready Ready Preview Comment Oct 14, 2025 11:41pm
next-chat-template Ready Ready Preview Comment Oct 14, 2025 11:41pm
react-boilerplate Ready Ready Preview Comment Oct 14, 2025 11:41pm
react-chat Ready Ready Preview Comment Oct 14, 2025 11:41pm
react-image Ready Ready Preview Comment Oct 14, 2025 11:41pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
component-registry Skipped Skipped Oct 14, 2025 11:41pm

Copy link

railway-app bot commented Oct 9, 2025

🚅 Deployed to the echo-pr-541 environment in echo

Service Status Web Updated (UTC)
echo ◻️ Removed (View Logs) Web Oct 15, 2025 at 12:31 am

metadata: {
providerId: output.request_id,
provider: 'tavily',
model: input.search_depth ?? 'basic',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not, but it will get pretty annoying to always add n+1 resources if all usage is going to be A. recorded in echo tx and B. need it's own fields.

We should abandon supporting echo balance or just be okay with overloading

input: TavilySearchInput,
output: TavilySearchOutput,
cost: Decimal
): Transaction => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole tx object is throwing me off. why do we need this for x402? (or are we allowing echo credits)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're allowing creds

Comment on lines +40 to +41
buildX402Response(req, res, maxCost);
return undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont get this change. we are no longer returning the 402 response?

Comment on lines 157 to 160
await transfer(
authPayload.from as `0x${string}`,
refundAmountUsdcBigInt
).catch(transferError => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just a promise returning a promise? why are we .catch()

Comment on lines 24 to 25
buildX402Response(req, res, maxCost);
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i may have introduced this pattern to fix the 502 thing but we should prob bubble the 402 response up instead of writing to res deep in this call stack then return nothing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed but no reason to block this PR on this, it's an issue in the codebase otherwise

Comment on lines +34 to +47
} else if (isX402Request(headers)) {
const settleResult = await settle(req, res, headers, maxCost);
if (!settleResult) {
return buildX402Response(req, res, maxCost);
}
const { payload, paymentAmountDecimal } = settleResult;

const output = await tavilySearch(inputBody);

const transaction = createTavilyTransaction(inputBody, output, maxCost);

await finalize(paymentAmountDecimal, transaction, payload);

return res.status(200).json(output);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for X402 payments - if the Tavily API call fails after payment settlement, users won't receive refunds.

View Details
📝 Patch Details
diff --git a/packages/app/server/src/resources/tavily/route.ts b/packages/app/server/src/resources/tavily/route.ts
index b8f9c685..cb322777 100644
--- a/packages/app/server/src/resources/tavily/route.ts
+++ b/packages/app/server/src/resources/tavily/route.ts
@@ -1,10 +1,10 @@
-import { buildX402Response, isApiRequest, isX402Request } from 'utils';
+import { buildX402Response, isApiRequest, isX402Request, decimalToUsdcBigInt } from 'utils';
 import { TavilySearchInputSchema } from './types';
 import { calculateTavilySearchCost, tavilySearch } from './tavily';
 import { authenticateRequest } from 'auth';
 import { prisma } from 'server';
-import { settle } from 'handlers';
-import { finalize } from 'handlers';
+import { settle, finalize } from 'handlers';
+import { transfer } from 'transferWithAuth';
 import { createTavilyTransaction } from './tavily';
 import logger from 'logger';
 import { Request, Response } from 'express';
@@ -38,13 +38,21 @@ export async function tavilySearchRoute(req: Request, res: Response) {
       }
       const { payload, paymentAmountDecimal } = settleResult;
 
-      const output = await tavilySearch(inputBody);
+      try {
+        const output = await tavilySearch(inputBody);
 
-      const transaction = createTavilyTransaction(inputBody, output, maxCost);
+        const transaction = createTavilyTransaction(inputBody, output, maxCost);
 
-      await finalize(paymentAmountDecimal, transaction, payload);
+        await finalize(paymentAmountDecimal, transaction, payload);
 
-      return res.status(200).json(output);
+        return res.status(200).json(output);
+      } catch (error) {
+        // Full refund on error, like the main handleX402Request handler does
+        const refundAmountUsdcBigInt = decimalToUsdcBigInt(paymentAmountDecimal);
+        const authPayload = payload.authorization;
+        await transfer(authPayload.from as `0x${string}`, refundAmountUsdcBigInt);
+        throw error;
+      }
     } else {
       return buildX402Response(req, res, maxCost);
     }

Analysis

Missing error handling in tavily X402 route causes payment loss on API failures

What fails: tavilySearchRoute() in X402 payment path lacks error handling after settle() - if tavilySearch() throws an error, user loses payment without receiving service

How to reproduce:

# Send X402 payment request to tavily endpoint with invalid/expired API key
curl -X POST /tavily/search \
  -H "Content-Type: application/json" \
  -H "X-Payment: <valid_payment_payload>" \
  -d '{"query": "test", "search_depth": "basic"}'

Result: Payment gets settled successfully, then tavilySearch() throws "Tavily API request failed: 401 Unauthorized" but user receives no refund - payment is lost

Expected: Should perform full refund like handleX402Request() does at handlers.ts:156-160 when errors occur after settlement

Root cause: Tavily route calls settle(), then tavilySearch() without try-catch protection, unlike main X402 handler which wraps post-settlement operations in try-catch with transfer() refund on errors

@rsproule rsproule merged commit 293fd59 into master Oct 15, 2025
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants