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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The application pairs a React frontend with a local Rust-powered EDB (EVM Debugg
| Styling | Tailwind CSS v4, shadcn/ui |
| Web3 | ethers v5, viem, wagmi, RainbowKit |
| Simulation | REVM (Rust), EDB engine, WebSocket bridge |
| Integrations | LI.FI SDK (Earn API, Composer), Gemini LLM |
| Integrations | LI.FI SDK (Earn API, Composer), BTL Runtime |
| API Proxies | Vercel Serverless Functions |
| Testing | Vitest, Testing Library |

Expand Down Expand Up @@ -83,7 +83,7 @@ A full yield management layer powered by the LI.FI Earn API:

An AI assistant that translates natural language yield goals into actionable vault recommendations:

- **Intent Parser** -- Gemini LLM converts free-text prompts ("safest USDC vault above 5% on Arbitrum") into structured filters (token, chain, APY range, objective, protocol allow/deny lists).
- **Intent Parser** -- BTL Runtime converts free-text prompts ("safest USDC vault above 5% on Arbitrum") into structured filters (token, chain, APY range, objective, protocol allow/deny lists).
- **My Assets Mode** -- Say "best vaults for my assets" and the concierge fans out per-asset recommendations for every idle token in the connected wallet.
- **Consolidate Mode** -- Say "best vault for my assets" (singular) and the concierge finds the top vault candidates to funnel all holdings into a single position via cross-chain swaps.
- **Idle Sweep** -- Detects wallet tokens sitting idle (not earning yield) and suggests the best vault for each, with one-click deposit.
Expand Down Expand Up @@ -170,9 +170,9 @@ For the LI.FI Earn integration and AI concierge, set the following in `.env`:
| Variable | Purpose |
|----------|---------|
| `LIFI_API_KEY` | LI.FI API key for Earn and Composer endpoints |
| `GEMINI_API_KEY` | Google AI Studio API key for the yield concierge LLM |
| `GEMINI_MODEL` | Primary Gemini model (default: `gemini-3.1-pro-preview`) |
| `GEMINI_FALLBACK_MODEL` | Fallback on 429/503 (default: `gemini-2.5-flash`) |
| `BTL_API_KEY` | BTL Runtime API key for the yield concierge LLM |
| `BTL_MODEL` | BTL model (default: `deepseek-v4-flash`) |
| `BTL_BASE_URL` | BTL Runtime base URL (default: `https://api.badtheorylabs.com`) |
Comment on lines +173 to +175
| `PROXY_SECRET` | Shared secret for API proxy authentication (production) |
| `ALLOWED_ORIGINS` | Comma-separated allowed CORS origins (production) |

Expand Down Expand Up @@ -229,7 +229,7 @@ src/
lib/ Shared libraries

api/
llm-recommend.ts Gemini LLM proxy with model fallback
llm-recommend.ts BTL Runtime proxy with model fallback
lifi-composer.ts LI.FI Composer quote/execute proxy
edb/ EDB simulation API routes

Expand Down
44 changes: 30 additions & 14 deletions api/llm-recommend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ export const config = {
maxDuration: 60,
};

const GEMINI_MODEL = process.env.GEMINI_MODEL || "gemini-2.5-flash-lite";
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
const BTL_MODEL = process.env.BTL_MODEL || "gpt-4o-mini";
const BTL_API_KEY = process.env.BTL_API_KEY || "";
const BTL_BASE_URL = process.env.BTL_BASE_URL || "https://api.badtheorylabs.com";

const ALLOWED_METHODS = new Set(["POST", "OPTIONS"]);
const ALLOWED_ORIGINS = new Set(
Expand Down Expand Up @@ -68,41 +69,56 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
return res.status(400).json({ error: "Missing JSON body" });
}

if (!Array.isArray((body as any).contents)) {
return res.status(400).json({ error: "Body must include `contents` array" });
if (!Array.isArray((body as any).messages)) {
return res.status(400).json({ error: "Body must include `messages` array" });
}

const serialized = JSON.stringify(body);
if (serialized.length > MAX_BODY_BYTES) {
return res.status(413).json({ error: "Request body too large" });
}
Comment on lines +72 to 79
Comment on lines 76 to 79

if (!GEMINI_API_KEY) {
return res.status(500).json({ error: "No GEMINI_API_KEY configured" });
if (!BTL_API_KEY) {
return res.status(500).json({ error: "No BTL_API_KEY configured" });
}

const geminiHeaders: Record<string, string> = {
const btlHeaders: Record<string, string> = {
"Content-Type": "application/json",
"x-goog-api-key": GEMINI_API_KEY,
Authorization: `Bearer ${BTL_API_KEY}`,
};

const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
const url = `${BTL_BASE_URL}/v1/chat/completions`;

try {
const upstreamRes = await fetch(url, {
method: "POST",
headers: geminiHeaders,
headers: btlHeaders,
body: serialized,
signal: AbortSignal.timeout(55_000),
});

const text = await upstreamRes.text();

if (allowedOrigin) {
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
}
if (allowedOrigin) res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
res.setHeader("Content-Type", "application/json");
res.setHeader("X-Gemini-Model", GEMINI_MODEL);

// Forward BTL cost/routing headers so the browser can render AiCostChip.
const BTL_HEADERS = [
"x-btl-benchmark-cost",
"x-btl-customer-charge",
"x-btl-saved",
"x-gateway-fee-pct",
"x-gateway-cost",
"x-request-id",
];
for (const h of BTL_HEADERS) {
const v = upstreamRes.headers.get(h);
if (v) res.setHeader(h, v);
}
const requestedModel = (body as any)?.model || BTL_MODEL;
res.setHeader("X-BTL-Model", String(requestedModel));
res.setHeader("Access-Control-Expose-Headers", [...BTL_HEADERS, "x-btl-model"].join(", "));

return res.status(upstreamRes.status).send(text);
} catch (err: any) {
console.error("[llm-recommend] upstream error:", err);
Expand Down
8 changes: 8 additions & 0 deletions public/logos/btl-runtime.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions src/components/BtlBadge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.btl-badge {
display: inline-flex;
align-items: center;
gap: 7px;
text-decoration: none;
color: #b8b8b8;
transition: color 0.15s ease, opacity 0.15s ease;
line-height: 1;
}
.btl-badge:hover {
color: #f2f2f0;
}
.btl-badge-logo {
display: block;
flex-shrink: 0;
border-radius: 5px;
/* The mark is a near-black rounded square; a hairline ring defines its
edge on HexKit's dark background so it reads as a real logo, not a blob. */
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.btl-badge:hover .btl-badge-logo {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.32);
}
.btl-badge-label {
font-family: ui-monospace, "JetBrains Mono", "Geist Mono", Menlo, monospace;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 600;
white-space: nowrap;
}
41 changes: 41 additions & 0 deletions src/components/BtlBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "./BtlBadge.css";

const BTL_URL = "https://runtime.badtheorylabs.com";

interface BtlBadgeProps {
className?: string;
showLabel?: boolean;
}

/**
* "Powered by BTL" strip — shown wherever the app calls the BTL Runtime,
* mirroring EdbBadge. Uses the official BTL Runtime mark (public/logos).
*/
export function BtlBadge({ className = "", showLabel = true }: BtlBadgeProps) {
return (
<a
href={BTL_URL}
target="_blank"
rel="noopener noreferrer"
title="Powered by BTL Runtime → runtime.badtheorylabs.com"
aria-label="Powered by BTL Runtime · opens runtime.badtheorylabs.com in a new tab"
className={`btl-badge ${className}`}
>
<img
src="/logos/btl-runtime.svg"
alt=""
aria-hidden="true"
width={20}
height={20}
className="btl-badge-logo"
/>
{showLabel && (
<span className="btl-badge-label">
Powered by BTL <span aria-hidden="true">↗</span>
</span>
)}
</a>
);
}

export default BtlBadge;
35 changes: 35 additions & 0 deletions src/components/btl/AiCostChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Receipt } from "@phosphor-icons/react";
import type { BtlRuntimeMeta } from "@/lib/btl/client";

/**
* A per-call cost receipt — the cost-transparency differentiator. The $ amount
* is the headline (green "≈ $0" for the free routes); model + fee are metadata.
*/
export function AiCostChip({ meta }: { meta?: BtlRuntimeMeta | null }) {
if (!meta || meta.customerChargeUsd == null) return null;
const free = meta.customerChargeUsd < 0.0001;
const cost = free ? "≈ $0" : `$${meta.customerChargeUsd.toFixed(4)}`;
return (
<span
className="inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-background/40 px-2 py-0.5 font-mono text-[9px] leading-none text-muted-foreground"
title={`Routed via BTL Runtime · $${meta.customerChargeUsd.toFixed(6)}${meta.requestId ? ` · ${meta.requestId}` : ""}`}
>
<Receipt weight="duotone" aria-hidden className="h-3 w-3 shrink-0 opacity-70" />
<span className={`text-[11px] font-semibold ${free ? "text-emerald-400" : "text-foreground"}`}>
{cost}
</span>
{meta.model && (
<>
<span className="opacity-30">·</span>
<span>{meta.model}</span>
</>
)}
{meta.feePct != null && (
<>
<span className="opacity-30">·</span>
<span className="tabular-nums">{meta.feePct}% gw</span>
</>
)}
</span>
);
}
Loading