Skip to content
Merged
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
47 changes: 45 additions & 2 deletions apps/server/src/config/mcp-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,51 @@ export const mcpServers: McpServerConfig[] = [
"get-tokens",
"get-token",
"get-status",
"get-token-balance",
"get-native-token-balance",
"get-allowance",
"get-chain-by-id",
"get-chain-by-name",
"get-connections",
"get-routes",
],
systemMessage:
"The user asked you to use LI.FI. To perform the requested action, first resolve the chain names they provided by calling get-chains. Then resolve the token details by calling get-tokens or get-token. Only after that, request a quote using the resolved values. The quote must be returned as a transaction request inside a fenced code block. The fenced block identifier must be tx (```tx). Ensure the `to`, `value`, and `data` fields are included and complete. The transaction is sponsored by Router402, so the user does not need to hold any native gas token.",
systemMessage: `You are a DeFi assistant using LI.FI to help the user swap and bridge tokens across chains.

User's wallet address: {{WALLET_ADDRESS}}

Workflow:
1. Validate the request: If the user asks for a swap or bridge, ensure they have provided:
- Source token and destination token
- Amount to swap/bridge
- Source chain and destination chain (if bridging)
If the amount is missing, always ask the user to specify the amount before proceeding. Do not assume or infer amounts.

2. Resolve chains and tokens:
- Resolve chain name(s) to chain IDs.
- Resolve token symbols to contract addresses and decimals.

3. Check user balance:
- Check the balance of the source token for the user's wallet address ({{WALLET_ADDRESS}}) on the relevant chain.
- If the user's balance is insufficient for the requested amount, inform them of their current balance and do not proceed with the quote.

4. Check token allowance (ERC20 tokens only, skip for native tokens):
- Use get-allowance to check whether the user's wallet ({{WALLET_ADDRESS}}) has approved sufficient spending for the source token on the relevant chain.
- If the allowance is insufficient, return an approval transaction first for the user to sign before proceeding with the swap/bridge.
- After the user confirms the approval transaction has been executed, proceed to the quote step.

5. Request a quote:
- Only after the amount is confirmed, balance is verified sufficient, and allowance is confirmed adequate, request a quote with the resolved chain IDs, token addresses, and the amount (converted to the token's smallest unit using its decimals).
- Return only the transaction request from the quote response.

6. Return the transaction:
- Present the transaction inside a fenced code block with the tx identifier.
- Include only the to, value, and data fields from the actual quote response.
- Never fabricate, estimate, or return placeholder/dummy transaction data. If the quote fails or returns an error, report the error to the user instead.

Rules:
- Never generate transaction data yourself. All to, value, and data fields must come directly from the LI.FI API response.
- If any API call fails, explain the error clearly rather than returning made-up data.
- Do not execute any transaction — only return the transaction request for the user to review and sign.
- Use the available LI.FI tools to accomplish each step. Do not hardcode or guess chain IDs, token addresses, or any other values.`,
},
];
15 changes: 13 additions & 2 deletions apps/server/src/hooks/x402/handlers/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import { logger } from "@router402/utils";
import { decodePaymentSignatureHeader } from "@x402/core/http";
import type { HTTPRequestContext, RouteConfig } from "@x402/core/server";
import type { PaymentPayload } from "@x402/core/types";
import { verifyToken } from "../../../services/auth.service.js";
import {
getSmartAccountAddress,
verifyToken,
} from "../../../services/auth.service.js";
import { autoPayDebt } from "../../../services/auto-payment.js";
import { getUserDebt, isDebtBelowThreshold } from "../../../services/debt.js";
import { setWalletAddress } from "../../../utils/request-context.js";
import {
setSmartAccountAddress,
setWalletAddress,
} from "../../../utils/request-context.js";
import {
extractWalletFromPayload,
verifyPaymentSignature,
Expand Down Expand Up @@ -94,13 +100,17 @@ export async function onProtectedRequest(
chainId,
});

// Look up smart account address for MCP system prompt injection
const smartAccountAddr = await getSmartAccountAddress(userId);

// Check if debt is below threshold (from database)
// Task 3.2 will add: if debt >= threshold, trigger autoPayDebt()
const belowThreshold = await isDebtBelowThreshold(walletAddress);

if (belowThreshold) {
// Store wallet in async context for usage tracking
setWalletAddress(walletAddress);
if (smartAccountAddr) setSmartAccountAddress(smartAccountAddr);

hookLogger.info("Access granted via JWT - debt below threshold", {
wallet: walletAddress.slice(0, 10),
Expand Down Expand Up @@ -131,6 +141,7 @@ export async function onProtectedRequest(
// Auto-payment successful - grant access
// Validates: Requirement 5.2
setWalletAddress(walletAddress);
if (smartAccountAddr) setSmartAccountAddress(smartAccountAddr);

hookLogger.info("Access granted via JWT - auto-payment successful", {
wallet: walletAddress.slice(0, 10),
Expand Down
24 changes: 18 additions & 6 deletions apps/server/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import {
translateProviderError,
} from "../utils/errors.js";
import { calculateCost, isSupportedModel } from "../utils/pricing.js";
import { getWalletAddress } from "../utils/request-context.js";
import {
getSmartAccountAddressFromContext,
getWalletAddress,
} from "../utils/request-context.js";

const chatLogger = logger.context("ChatRoute");

Expand Down Expand Up @@ -87,18 +90,25 @@ export function createChatRouter(): Router {

// Get wallet from AsyncLocalStorage (set by x402 hook)
const walletAddress = getWalletAddress();
// Use smart account address for MCP prompt (fallback to EOA)
const mcpWalletAddress =
getSmartAccountAddressFromContext() ?? walletAddress;

if (request.stream) {
// Handle streaming response (Requirements 6.1, 6.2, 6.3, 6.4)
await handleStreamingResponse(
res,
chatService,
request,
walletAddress
walletAddress,
mcpWalletAddress
);
} else {
// Handle non-streaming response
const response = await chatService.complete(request, walletAddress);
const response = await chatService.complete(
request,
mcpWalletAddress
);

// Debug log for usage tracking
chatLogger.debug("Usage tracking check", {
Expand Down Expand Up @@ -166,7 +176,8 @@ export function createChatRouter(): Router {
* @param res - Express response object
* @param chatService - Chat service instance
* @param request - Validated chat completion request
* @param walletAddress - Optional wallet address for usage tracking
* @param walletAddress - Optional EOA wallet address for usage tracking
* @param mcpWalletAddress - Optional smart account address for MCP prompt injection
*
* @see Requirement 6.1 - Set response headers for SSE
* @see Requirement 6.3 - Format each chunk as 'data: {json}\n\n'
Expand All @@ -176,7 +187,8 @@ async function handleStreamingResponse(
res: Response,
chatService: ChatService,
request: ReturnType<typeof ChatCompletionRequestSchema.parse>,
walletAddress?: string
walletAddress?: string,
mcpWalletAddress?: string
): Promise<void> {
// Set SSE headers (Requirement 6.1)
res.setHeader("Content-Type", "text/event-stream");
Expand All @@ -192,7 +204,7 @@ async function handleStreamingResponse(
| undefined;

// Stream chunks from the chat service (Requirements 6.2, 6.3)
for await (const chunk of chatService.stream(request, walletAddress)) {
for await (const chunk of chatService.stream(request, mcpWalletAddress)) {
// Capture usage from final chunk
if (chunk.usage) {
finalUsage = chunk.usage;
Expand Down
15 changes: 15 additions & 0 deletions apps/server/src/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,21 @@ export async function authorize(
};
}

/**
* Get the smart account address for a user by userId.
* Returns null if the user has no session key record.
*/
export async function getSmartAccountAddress(
userId: string
): Promise<string | null> {
const db = getPrisma();
const record = await db.sessionKeyRecord.findUnique({
where: { userId },
select: { smartAccountAddress: true },
});
return record?.smartAccountAddress ?? null;
}

/**
* Verify a JWT token and return the payload
*/
Expand Down
13 changes: 4 additions & 9 deletions apps/server/src/services/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,20 +319,15 @@ export class ChatService {
if (!mcpManager.hasServers()) return messages;

const systemMessages = mcpManager.getSystemMessages();
if (systemMessages.length === 0 && !walletAddress) return messages;
if (systemMessages.length === 0) return messages;

const injected: Message[] = systemMessages.map((content) => ({
role: "system" as const,
content,
content: walletAddress
? content.replace(/\{\{WALLET_ADDRESS\}\}/g, walletAddress)
: content,
}));

if (walletAddress) {
injected.push({
role: "system" as const,
content: `User's wallet address: ${walletAddress}`,
});
}

return [...injected, ...messages];
}

Expand Down
18 changes: 18 additions & 0 deletions apps/server/src/utils/request-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AsyncLocalStorage } from "node:async_hooks";

interface RequestContext {
walletAddress?: string;
smartAccountAddress?: string;
}

export const requestContext = new AsyncLocalStorage<RequestContext>();
Expand All @@ -29,3 +30,20 @@ export function setWalletAddress(address: string): void {
store.walletAddress = address.toLowerCase();
}
}

/**
* Get the current smart account address from request context
*/
export function getSmartAccountAddressFromContext(): string | undefined {
return requestContext.getStore()?.smartAccountAddress;
}

/**
* Set the smart account address in request context
*/
export function setSmartAccountAddress(address: string): void {
const store = requestContext.getStore();
if (store) {
store.smartAccountAddress = address.toLowerCase();
}
}
Loading