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
4 changes: 4 additions & 0 deletions kits/smc-bias-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
LAMATIC_API_KEY=lt-a37857b83dd6f993e9d327b9a9f68bba
LAMATIC_GRAPHQL_URL=https://ayushsorganization263-ayushsproject211.lamatic.dev/graphql
LAMATIC_PROJECT_ID=3bffc6da-08d8-4f1a-af0c-f654bca169e6
LAMATIC_WORKFLOW_ID=8ba7821c-251d-4d58-827b-bb5d627a3d57
Comment on lines +1 to +4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Abort, abort — this tape is self-destructing for a reason, and these credentials should too.

These values look like real, issued credentials (API key, project ID, workflow ID, and a personal Lamatic subdomain in the GraphQL URL), not placeholders. Static analysis flags a generic API key here. Committing real secrets into a .env.example template exposes them to anyone who clones the repo.

Rotate this key/workflow immediately and replace all four values with clearly fake placeholders (e.g. LAMATIC_API_KEY=your-lamatic-api-key).

🧰 Tools
🪛 Betterleaks (1.6.1)

[high] 1-1: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 dotenv-linter (4.0.0)

[warning] 4-4: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/smc-bias-agent/.env.example` around lines 1 - 4, Replace the
credential-like values in the .env.example entries LAMATIC_API_KEY,
LAMATIC_GRAPHQL_URL, LAMATIC_PROJECT_ID, and LAMATIC_WORKFLOW_ID with clearly
fake placeholders, and rotate or revoke the exposed Lamatic credentials
separately.

Source: Linters/SAST tools

4 changes: 4 additions & 0 deletions kits/smc-bias-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.next
.env.local
*.tsbuildinfo
55 changes: 55 additions & 0 deletions kits/smc-bias-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# SMC Bias Agent

Detects Smart Money Concepts (SMC/ICT) structure on a crypto pair — swing highs/lows, order blocks, fair value gaps, break of structure / change of character — and returns a plain-English directional bias with the key levels to watch. Automates the manual chart marking an SMC/ICT trader does by hand.

## How it works

1. **Data** — pulls recent OHLCV candles from Binance's public REST API (no key required).
2. **Detection** (`lib/smc.ts`) — deterministic, rule-based structure analysis:
- Swing highs/lows via a 2-candle fractal
- Order blocks: the last opposite-colored candle before a confirmed structure break
- Fair value gaps: 3-candle imbalance where the outer candles don't overlap
- Break of structure (trend continuation) vs. change of character (trend reversal)
- A bias + confidence score from the most recent structure event and any still-unmitigated order block or unfilled FVG in that direction
3. **Narrative** (`lib/lamatic-client.ts`) — the structured detection output is handed to an LLM to explain in plain English. If `LAMATIC_API_KEY` / `LAMATIC_FLOW_ENDPOINT` aren't set, a local deterministic template produces the same kind of explanation instead, so the kit is fully runnable with zero configuration.
4. **UI** (`app/page.tsx`, `components/`) — a symbol/timeframe form, an annotated candlestick chart with the detected zones overlaid, and a bias card with reasoning and key levels.

## Setup

```bash
cd kits/smc-bias-agent
npm install
cp .env.example .env.local # optional, see below
npm run dev
```

Open `http://localhost:3000`, enter a symbol (e.g. `BTCUSDT`), pick a timeframe, and scan.

## Wiring up your Lamatic flow (optional but recommended for submission)

The kit works with zero setup using the local fallback narrative. To use an actual Lamatic flow instead:

1. Create a project in [Lamatic Studio](https://studio.lamatic.ai) and build a simple flow: one input (a prompt string) → one LLM node.
2. The exact prompt to paste into the LLM node is generated by `buildBiasPrompt()` in `lib/lamatic-client.ts` — log it once locally and copy it in, or point the node at the same structure.
3. Copy the flow's execution endpoint and an API key into `.env.local`:
```
LAMATIC_API_KEY=...
LAMATIC_FLOW_ENDPOINT=...
```
4. Restart `npm run dev` — the bias card footer will confirm whether it's using the Lamatic flow or the local fallback.

> The request/response shape in `lib/lamatic-client.ts` is a reasonable placeholder — swap in the exact call snippet Studio generates for your flow if the field names differ.

## Testing the detection logic

```bash
npm run test:smc
```

Runs `scripts/test-smc.ts` against a hand-built synthetic candle series with a known downtrend, order block, FVG, and reversal, and asserts the detector finds all of them.

## Notes and limitations

- Swing/OB/FVG detection uses simplified, commonly-used definitions (2-candle fractals, body-based OB zones) rather than a specific proprietary indicator's exact rules — reasonable for demonstrating the concept, not tuned for live trading.
- This is an analysis and explanation tool, not trading advice — it never suggests position sizing, entries, or execution.
- Only a small set of intervals are enabled (`15m`, `1h`, `4h`, `1d`); extend `ALLOWED_INTERVALS` in `lib/binance.ts` if you need others.
32 changes: 32 additions & 0 deletions kits/smc-bias-agent/app/api/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { fetchCandles } from "@/lib/binance";
import { analyzeSMC } from "@/lib/smc";
import { getBiasNarrative } from "@/lib/lamatic-client";

export async function POST(req: NextRequest) {
let body: { symbol?: string; interval?: string };

try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Request body must be JSON." }, { status: 400 });
}

const symbol = (body.symbol || "").trim().toUpperCase();
const interval = (body.interval || "4h").trim();

if (!symbol) {
return NextResponse.json({ error: "Provide a symbol, e.g. BTCUSDT." }, { status: 400 });
}

try {
const candles = await fetchCandles(symbol, interval);
const analysis = analyzeSMC(symbol, interval, candles);
const narrative = await getBiasNarrative(analysis);

return NextResponse.json({ analysis, narrative });
} catch (err) {
const message = err instanceof Error ? err.message : "Unexpected error while analyzing the symbol.";
return NextResponse.json({ error: message }, { status: 502 });
}
}
Comment on lines +22 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

One-size-fits-all disguise won't fool every checkpoint.

Every thrown error — including client-input problems like an unsupported interval or "not enough candles" — is mapped to 502. That's a Bad-Gateway code reserved for upstream failures; input-validation failures should surface as 400.

🎯 Proposed fix: distinguish client vs upstream errors
   } catch (err) {
     const message = err instanceof Error ? err.message : "Unexpected error while analyzing the symbol.";
-    return NextResponse.json({ error: message }, { status: 502 });
+    const isClientError = message.includes("Unsupported interval") || message.includes("Not enough candles");
+    return NextResponse.json({ error: message }, { status: isClientError ? 400 : 502 });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const candles = await fetchCandles(symbol, interval);
const analysis = analyzeSMC(symbol, interval, candles);
const narrative = await getBiasNarrative(analysis);
return NextResponse.json({ analysis, narrative });
} catch (err) {
const message = err instanceof Error ? err.message : "Unexpected error while analyzing the symbol.";
return NextResponse.json({ error: message }, { status: 502 });
}
}
try {
const candles = await fetchCandles(symbol, interval);
const analysis = analyzeSMC(symbol, interval, candles);
const narrative = await getBiasNarrative(analysis);
return NextResponse.json({ analysis, narrative });
} catch (err) {
const message = err instanceof Error ? err.message : "Unexpected error while analyzing the symbol.";
const isClientError = message.includes("Unsupported interval") || message.includes("Not enough candles");
return NextResponse.json({ error: message }, { status: isClientError ? 400 : 502 });
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@kits/smc-bias-agent/app/api/analyze/route.ts` around lines 22 - 32, Update
the error handling in the analyze route’s catch block to distinguish
client-input/validation failures, such as unsupported interval or insufficient
candles, from upstream fetch or analysis failures. Return HTTP 400 for client
errors and retain 502 for upstream failures, using the existing error
information and route flow without changing successful responses.

62 changes: 62 additions & 0 deletions kits/smc-bias-agent/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
color-scheme: dark;
}

* {
box-sizing: border-box;
}

html,
body {
padding: 0;
margin: 0;
background: #12141a;
background-image:
radial-gradient(circle at 15% 0%, rgba(216, 166, 87, 0.06), transparent 45%),
radial-gradient(circle at 85% 100%, rgba(79, 174, 138, 0.05), transparent 45%);
background-attachment: fixed;
color: #e4e6eb;
}

::selection {
background: rgba(216, 166, 87, 0.3);
}

:focus-visible {
outline: 2px solid #d8a657;
outline-offset: 2px;
border-radius: 4px;
}

.tabular {
font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}

@keyframes fade-up {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.animate-fade-up {
animation: fade-up 0.35s ease-out both;
}
34 changes: 34 additions & 0 deletions kits/smc-bias-agent/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Space_Grotesk, Inter, IBM_Plex_Mono } from "next/font/google";
import "./globals.css";

const display = Space_Grotesk({
subsets: ["latin"],
weight: ["500", "700"],
variable: "--font-display",
});

const body = Inter({
subsets: ["latin"],
weight: ["400", "500", "600"],
variable: "--font-body",
});

const mono = IBM_Plex_Mono({
subsets: ["latin"],
weight: ["400", "500"],
variable: "--font-mono",
});

export const metadata: Metadata = {
title: "SMC Bias Agent",
description: "Smart Money Concepts structure detection and plain-English bias for any crypto pair.",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${display.variable} ${body.variable} ${mono.variable}`}>
<body className="font-body min-h-screen">{children}</body>
</html>
);
}
125 changes: 125 additions & 0 deletions kits/smc-bias-agent/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import { useState } from "react";
import PriceChart from "@/components/PriceChart";
import BiasCard from "@/components/BiasCard";
import { SMCAnalysis } from "@/lib/types";

const INTERVALS = ["15m", "1h", "4h", "1d"];

interface ApiResponse {
analysis: SMCAnalysis;
narrative: { reasoning: string; source: "lamatic" | "fallback" };
}

export default function Home() {
const [symbol, setSymbol] = useState("BTCUSDT");
const [interval, setIntervalValue] = useState("4h");
const [data, setData] = useState<ApiResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

async function runScan(e?: React.FormEvent) {
e?.preventDefault();
setLoading(true);
setError(null);

try {
const res = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ symbol, interval }),
});
const json = await res.json();

if (!res.ok) throw new Error(json.error || "Something went wrong.");
setData(json);
} catch (err) {
setError(err instanceof Error ? err.message : "Something went wrong.");
setData(null);
} finally {
setLoading(false);
}
}

return (
<main className="mx-auto max-w-5xl px-4 py-8 md:py-12 flex flex-col gap-8">
<header className="flex flex-col gap-1">
<span className="font-mono text-xs uppercase tracking-[0.2em] text-signal">Smart Money Concepts</span>
<h1 className="font-display text-2xl md:text-3xl font-medium text-ink">SMC Bias Agent</h1>
<p className="text-sm text-muted max-w-xl">
Reads recent price action for a pair, marks up order blocks, fair value gaps, and structure breaks the way
an ICT/SMC trader would by hand, then explains the bias in plain English.
</p>
</header>

<form onSubmit={runScan} className="flex flex-wrap items-end gap-3 rounded-lg border border-border bg-panel p-4">
<label className="flex flex-col gap-1">
<span className="text-xs uppercase tracking-wide text-muted">Symbol</span>
<input
value={symbol}
onChange={(e) => setSymbol(e.target.value.toUpperCase())}
placeholder="BTCUSDT"
className="w-40 rounded-md border border-border bg-bg px-3 py-2 font-mono text-sm text-ink outline-none focus:border-signal"
/>
</label>

<label className="flex flex-col gap-1">
<span className="text-xs uppercase tracking-wide text-muted">Timeframe</span>
<select
value={interval}
onChange={(e) => setIntervalValue(e.target.value)}
className="rounded-md border border-border bg-bg px-3 py-2 font-mono text-sm text-ink outline-none focus:border-signal"
>
{INTERVALS.map((i) => (
<option key={i} value={i}>
{i}
</option>
))}
</select>
</label>

<button
type="submit"
disabled={loading}
className="ml-auto rounded-md bg-signal px-5 py-2 font-display text-sm font-medium text-bg transition hover:opacity-90 disabled:opacity-50"
>
{loading ? "Scanning…" : "Scan"}
</button>
</form>

{error && (
<div className="rounded-md border border-bear/40 bg-bear/10 px-4 py-3 text-sm text-ink">
Couldn&apos;t analyze that symbol: {error}
</div>
)}

{data && (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6 items-start">
<div className="animate-fade-up rounded-lg border border-border bg-panel p-4">
<PriceChart analysis={data.analysis} />
<div className="flex gap-4 pt-3 text-xs text-muted">
<span className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-sm bg-signal/60" /> order block
</span>
<span className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-sm bg-bull/40" /> fair value gap
</span>
<span className="flex items-center gap-1.5">
<span className="h-2 w-0.5 border-t border-dashed border-signal" /> structure break
</span>
</div>
</div>

<BiasCard analysis={data.analysis} reasoning={data.narrative.reasoning} source={data.narrative.source} />
</div>
)}

{!data && !error && (
<div className="rounded-lg border border-dashed border-border p-10 text-center text-sm text-muted">
Run a scan to see the detected structure.
</div>
)}
</main>
);
}
57 changes: 57 additions & 0 deletions kits/smc-bias-agent/components/BiasCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { SMCAnalysis } from "@/lib/types";

const BIAS_STYLES: Record<string, { text: string; bg: string; dot: string }> = {
bullish: { text: "text-bull", bg: "bg-bull/10", dot: "bg-bull" },
bearish: { text: "text-bear", bg: "bg-bear/10", dot: "bg-bear" },
neutral: { text: "text-muted", bg: "bg-muted/10", dot: "bg-muted" },
};

export default function BiasCard({
analysis,
reasoning,
source,
}: {
analysis: SMCAnalysis;
reasoning: string;
source: "lamatic" | "fallback";
}) {
const style = BIAS_STYLES[analysis.bias];

return (
<div className="animate-fade-up rounded-lg border border-border bg-panel p-5 flex flex-col gap-5">
<div className="flex items-center justify-between">
<div className={`inline-flex items-center gap-2 rounded-full px-3 py-1 ${style.bg}`}>
<span className={`h-2 w-2 rounded-full ${style.dot}`} />
<span className={`font-display font-medium uppercase tracking-wide text-sm ${style.text}`}>
{analysis.bias}
</span>
</div>
<span className="font-mono tabular text-sm text-muted">{analysis.confidence}% confidence</span>
</div>

<p className="text-sm leading-relaxed text-ink/90">{reasoning}</p>

<div className="flex flex-col gap-2">
<span className="text-xs uppercase tracking-wide text-muted">Key levels</span>
{analysis.keyLevels.length === 0 && <span className="text-sm text-muted">No active zones detected.</span>}
{analysis.keyLevels.map((level, i) => (
<div key={i} className="flex items-center justify-between border-t border-border pt-2 text-sm">
<div className="flex flex-col">
<span className="text-ink/90">{level.label}</span>
<span className="text-xs text-muted">{level.detail}</span>
</div>
<span className="font-mono tabular text-ink/90">
{level.low === level.high
? level.low.toFixed(level.low < 10 ? 4 : 1)
: `${level.low.toFixed(level.low < 10 ? 4 : 1)}–${level.high.toFixed(level.high < 10 ? 4 : 1)}`}
</span>
</div>
))}
</div>

<span className="text-xs text-muted">
{source === "lamatic" ? "Narrative generated by your Lamatic flow." : "Narrative generated locally (no LAMATIC_API_KEY configured)."}
</span>
</div>
);
}
Loading
Loading