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
4 changes: 2 additions & 2 deletions app/api/research/agent-flow/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createClient } from "@/lib/supabase/server";
import { aiJsonWithFallback, configuredAiProviders } from "@/lib/ai-fallback";
import { getBitgetAsset, getBitgetCandles, getBitgetStockPerp, STOCK_PERP_UNIVERSE } from "@/lib/bitget-market";
import { buildSignals, prepareBacktestCandles } from "@/lib/backtest";
import { extractFlowTickers, gateFlowTrade, normalizeFlowAssessments, sourceOnlyAssessment, type FlowAssessment, type FlowSource } from "@/lib/agent-flow-policy";
import { extractFlowTickers, gateFlowTrade, normalizeFlowAssessments, sourceOnlyAssessment, tokenizedMarketReview, type FlowAssessment, type FlowSource } from "@/lib/agent-flow-policy";
import { gatherIssuerEvidence, type FlowEvidence } from "@/lib/agent-flow-sources";
import { agentKeyIssuerEvidence } from "@/lib/agentkey-flow-sources";

Expand Down Expand Up @@ -163,7 +163,7 @@ export async function POST(request: Request) {
] : [];
stage("Alpha Factory", signals.length ? "complete" : "blocked", `${snap.ticker}: completed-candle rule`, signals.length ? `SMA 20/50 checked on ${completed.length} completed 4H bars; ${signals.at(-1) !== signals.at(-2) ? "fresh transition" : "no fresh transition"}.` : "Too few completed candles to test the fixed rule.", snap.ticker);
stage("Trading agent", gate.tradeable ? "complete" : "blocked", `${snap.ticker}: ${gate.tradeable ? "eligible for order review" : "research only"}`, gate.tradeable ? "Source coverage, market freshness, confidence, and the fixed Alpha rule align. You must still choose and confirm any action." : gateReasons.join(" "), snap.ticker);
return { ticker: snap.ticker, issuer: snap.issuer, summary: assessment.summary, stance: assessment.stance, confidence: assessment.confidence, confidenceCapped: Boolean(assessment.confidenceCapped),
return { ticker: snap.ticker, issuer: snap.issuer, summary: assessment.summary, stance: assessment.stance, confidence: assessment.confidence, confidenceCapped: Boolean(assessment.confidenceCapped), ...(tokenizedMarketQuestion ? { tokenizedMarketReview: tokenizedMarketReview({ ticker: snap.ticker, spotSymbol: snap.spot?.symbol ?? null, spotPrice: snap.spot?.lastPrice ?? null, spotTimestamp: snap.spot?.providerTimestamp ?? null, now }) } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the new finding through intelligence validation

When research is launched through the Overview or Tide Guide flows, IntelligenceProvider passes this payload through parseIntelligenceResult, whose IntelligenceAsset construction drops tokenizedMarketReview and the token-specific market fields; the registry UI consequently never receives or renders the explicit finding. Update that validated result path and its renderer as well, otherwise the new result appears only on the standalone /flow page despite these consumers using the same route.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict token findings to actual rToken questions

For ordinary stock questions containing a short word beginning with r, such as “Review NVDA,” “Rate NVDA,” or “What are the risks for NVDA?”, the existing tokenizedMarketQuestion regex matches that word and this new conditional now adds an rToken market finding. This makes non-token research explicitly claim the user asked about an rToken, so the condition should require a recognized R<TICKER> symbol or an explicit tokenized-market phrase rather than the broad flag.

Useful? React with 👍 / 👎.

tradeable: gate.tradeable, gateReasons, sources: snap.sources.slice(0, 20).map(({ title, url, publisher, publishedAt, via }): FlowSource => ({ title, url, publisher, publishedAt, via })),
market: { perpSymbol: snap.perp?.symbol ?? null, spotSymbol: snap.spot?.symbol ?? null, perpLastPrice: snap.perp?.lastPrice ?? null, perpProviderTimestamp: snap.perp?.providerTimestamp ?? null, spotLastPrice: snap.spot?.lastPrice ?? null, spotProviderTimestamp: snap.spot?.providerTimestamp ?? null, lastPrice: snap.perp?.lastPrice ?? null, providerTimestamp: snap.perp?.providerTimestamp ?? null, interval: "4H", candles: completed.slice(-30).map(candle => ({ timestamp: candle.timestamp, close: candle.close })) },
handoff: gate.tradeable ? { destinations, reason: "Eligible for a separate, user-confirmed order review. Paper futures and live rToken spot use different instruments and accounts." } : null,
Expand Down
2 changes: 1 addition & 1 deletion app/api/research/token-pulse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async function savePulseReplaySource(db: Awaited<ReturnType<typeof createClient>
const summary = { schemaVersion: 1, question, capturedAt: new Date().toISOString(), assets: [{ ticker: token, issuer: issuerAliases[token]?.[0] ?? token, summary: String(analysis.overview ?? "Public research snapshot."), stance, confidence: Math.max(0, Math.min(1, Number(analysis.confidence) || 0)), confidenceCapped: Number(analysis.confidence) < 0.4, tradeable: false, gateReasons: ["Token pulse is a research snapshot; it does not qualify a trading handoff."], sources, evidenceBalance: { supporting: [], counter: [] }, market: { spotSymbol: null, perpSymbol: null, lastPrice: null, providerTimestamp: null }, risks: Array.isArray(analysis.risks) ? analysis.risks : [], nextCheck: Array.isArray(analysis.watch_next) ? analysis.watch_next.join(" ") : null }], trace: [{ id: crypto.randomUUID(), agent: "Token Pulse", status: "complete", title: "Public news and community snapshot collected", at: new Date().toISOString() }] };
const { data: run, error } = await db.from("research_runs").insert({ user_id: userId, question, status: "complete", event_type: "agent_flow", summary, completed_at: new Date().toISOString() }).select("id").single();
if (error || !run) return null;
const { error: sourceError } = await db.from("research_sources").upsert(items.map(item => ({ research_run_id: run.id, url: item.url, title: item.title.slice(0, 300), publisher: item.publisher.slice(0, 120), published_at: item.publishedAt, source_type: item.channel.toLowerCase(), excerpt: item.snippet.slice(0, 1000) })), { onConflict: "research_run_id,url", ignoreDuplicates: true });
const { error: sourceError } = await db.from("research_sources").upsert(items.map(item => ({ research_run_id: run.id, url: item.url, title: item.title.slice(0, 300), publisher: item.publisher.slice(0, 120), published_at: item.publishedAt, source_type: "web", excerpt: item.snippet.slice(0, 1000) })), { onConflict: "research_run_id,url", ignoreDuplicates: true });
if (sourceError) { await db.from("research_runs").delete().eq("id", run.id).eq("user_id", userId); return null; }
return run.id as string;
}
Expand Down
1 change: 1 addition & 0 deletions app/flow/flow.css
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,4 @@
.flow-confidence-note{margin:-8px 0 14px;padding:9px 12px;border:1px solid color-mix(in srgb,var(--amber) 28%,var(--line));border-radius:10px;background:color-mix(in srgb,var(--amber) 7%,transparent);color:var(--muted);font:10px/1.5 var(--font)}
@media(max-width:560px){.flow-depth-badge{margin:7px 0 0}.flow-coverage{gap:8px 12px;padding:12px;border-radius:12px}.flow-coverage span:last-of-type{flex-basis:100%}}
.flow-market-references{display:grid;gap:7px;margin:14px 0;padding:13px 14px;border:1px solid var(--line);border-radius:12px;background:color-mix(in srgb,var(--panel) 76%,transparent)}.flow-market-references>b{color:var(--mint);font:9px var(--mono);letter-spacing:.1em;text-transform:uppercase}.flow-market-references>span{color:var(--muted);font:10px/1.55 var(--font)}
.flow-token-verdict{display:grid;gap:9px;margin:15px 0;padding:15px 16px;border:1px solid color-mix(in srgb,var(--flow-glow) 28%,var(--line));border-radius:13px;background:linear-gradient(135deg,color-mix(in srgb,var(--flow-glow) 9%,var(--panel)),color-mix(in srgb,var(--panel) 86%,transparent))}.flow-token-verdict>b{color:var(--text);font:600 13px/1.55 var(--font)}.flow-token-quote{width:fit-content;padding:5px 8px;border:1px solid var(--line);border-radius:999px;color:var(--muted);font:9px var(--mono);text-transform:uppercase;letter-spacing:.06em}.flow-token-quote-stale,.flow-token-quote-unavailable{border-color:color-mix(in srgb,var(--amber) 35%,var(--line));color:var(--amber)}.flow-token-quote-fresh{border-color:color-mix(in srgb,var(--mint) 35%,var(--line));color:var(--mint)}.flow-token-verdict ul{display:grid;gap:4px;margin:0;padding-left:17px;color:var(--muted);font:10px/1.5 var(--font)}
4 changes: 2 additions & 2 deletions app/ui/agent-flow-desk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type StageEvent = { id: string; agent: string; status: "running" | "complete" |
type Destination = { mode: "paper_futures" | "live_spot"; label: string; href: string };
type AssetRead = {
ticker: string; issuer: string; summary: string; stance: "bullish" | "bearish" | "mixed" | "unclear";
confidence: number; confidenceCapped?: boolean; tradeable: boolean; gateReasons: string[];
confidence: number; confidenceCapped?: boolean; tradeable: boolean; gateReasons: string[]; tokenizedMarketReview?: { verdict: string; quoteStatus: "unavailable" | "unverified" | "fresh" | "stale"; limitations: string[] };
evidenceBalance?: { supporting: Array<{ text: string; citedUrls: string[] }>; counter: Array<{ text: string; citedUrls: string[] }> };
sources: Array<{ title: string; url: string; publisher: string; publishedAt: string | null; via?: "Public feeds" | "AgentKey" }>;
market: { perpSymbol: string | null; spotSymbol: string | null; lastPrice: number | null; providerTimestamp?: number | null; perpLastPrice?: number | null; perpProviderTimestamp?: number | null; spotLastPrice?: number | null; spotProviderTimestamp?: number | null };
Expand Down Expand Up @@ -176,7 +176,7 @@ export default function AgentFlowDesk({ initialQuestion, evaluatorMode = false }
{error ? <div className="flow-error" role="alert"><b>Research paused</b><span>{error}</span>{error.toLowerCase().includes("sign in") ? <Link href="/login">Sign in ↗</Link> : <button type="button" onClick={() => void runFlow()} disabled={busy}>Try again ↗</button>}</div> : null}

{result ? <section className="flow-results" aria-label="Research findings"><div className="flow-section-head"><div><span className="flow-kicker">RESEARCH / {result.assets.length} {result.assets.length === 1 ? "ISSUER" : "ISSUERS"}</span><h2>One question. Separate decisions.</h2><p>Issuer coverage and token-market conditions are shown separately. Review the source links and quote timestamp before considering any next step.</p></div></div>
<div className="flow-asset-grid">{result.assets.map((asset) => <article key={asset.ticker} className="flow-asset-card"><header><div><span className="flow-kicker">{asset.ticker} / {asset.issuer}</span><h3>{asset.issuer}</h3></div><span className={`flow-verdict ${asset.tradeable ? "flow-verdict-ready" : ""}`}>{asset.tradeable ? "REVIEW AVAILABLE" : "NO TRADE HANDOFF"}</span></header><p>{asset.summary}</p><div className="flow-asset-meta"><span>Read <b>{asset.stance}</b></span><span>Model confidence <b>{Math.round(asset.confidence * 100)}%</b></span></div><section className="flow-market-references" aria-label={`${asset.ticker} instrument checks`}><b>Instrument checks</b><span>Tokenized spot · {asset.market.spotSymbol ?? `R${asset.ticker}USDT`} · {asset.market.spotLastPrice != null ? `$${asset.market.spotLastPrice.toLocaleString()}` : "No verified quote"}{asset.market.spotProviderTimestamp ? ` · ${new Date(asset.market.spotProviderTimestamp).toLocaleString()}` : " · freshness unverified"}</span><span>Stock perpetual · {asset.market.perpSymbol ?? `${asset.ticker}USDT`} · {asset.market.perpLastPrice != null ? `$${asset.market.perpLastPrice.toLocaleString()}` : "No verified quote"}{asset.market.perpProviderTimestamp ? ` · ${new Date(asset.market.perpProviderTimestamp).toLocaleString()}` : " · freshness unverified"} · derivative reference</span></section><p className="flow-confidence-note" role="note">{asset.confidenceCapped ? "Evidence is limited: the score is capped until two recent, distinct publishers support the assessment. " : ""}Model-reported score, not a calibrated probability.</p><QuoteFreshness market={asset.market} now={quoteNow} />{asset.evidenceBalance && (asset.evidenceBalance.supporting.length > 0 || asset.evidenceBalance.counter.length > 0) ? <section className="flow-evidence-balance" aria-label={`${asset.ticker} evidence balance`}><header><b>Evidence balance</b><span>Source-linked points only</span></header><div className="flow-evidence-columns">{([["Supports this reading", asset.evidenceBalance.supporting], ["Challenges this reading", asset.evidenceBalance.counter]] as const).map(([label, points]) => <section className="flow-evidence-side" key={label}><h4>{label}</h4>{points.length ? <ul>{points.map((point, index) => <li key={index}><p>{point.text}</p><div>{point.citedUrls.map((url) => { const source = asset.sources.find((item) => item.url === url); return source ? <a key={url} href={url} target="_blank" rel="noopener noreferrer">{source.publisher} ↗</a> : null; })}</div></li>)}</ul> : <p className="flow-evidence-empty">No source-linked points were established for this side.</p>}</section>)}</div></section> : null}{asset.gateReasons?.length ? <div className="flow-gates"><b>Decision gates</b><ul>{asset.gateReasons.map((reason, index) => <li key={index}>{reason}</li>)}</ul></div> : null}<details className="flow-sources"><summary>Inspect {asset.sources.length} {asset.sources.length === 1 ? "source" : "sources"}</summary>{asset.sources.length ? <ul>{asset.sources.map((source, index) => <li key={`${source.url}-${index}`}><span>{source.publisher}{source.publishedAt ? ` · ${new Date(source.publishedAt).toLocaleDateString()}` : ""}{source.via ? <em className="flow-source-via"> · {source.via}</em> : null}</span><a href={source.url} target="_blank" rel="noopener noreferrer">{source.title} ↗</a></li>)}</ul> : <p>No source links were returned; the trading gate remains closed.</p>}</details></article>)}</div>
<div className="flow-asset-grid">{result.assets.map((asset) => <article key={asset.ticker} className="flow-asset-card"><header><div><span className="flow-kicker">{asset.ticker} / {asset.issuer}</span><h3>{asset.issuer}</h3></div><span className={`flow-verdict ${asset.tradeable ? "flow-verdict-ready" : ""}`}>{asset.tradeable ? "REVIEW AVAILABLE" : "NO TRADE HANDOFF"}</span></header><p>{asset.summary}</p>{asset.tokenizedMarketReview ? <section className="flow-token-verdict" aria-label="Tokenized market finding"><span className="flow-kicker">R TOKEN / MARKET FINDING</span><b>{asset.tokenizedMarketReview.verdict}</b><span className={`flow-token-quote flow-token-quote-${asset.tokenizedMarketReview.quoteStatus}`}>Token quote freshness: {asset.tokenizedMarketReview.quoteStatus}</span><ul>{asset.tokenizedMarketReview.limitations.map((limitation) => <li key={limitation}>{limitation}</li>)}</ul></section> : null}<div className="flow-asset-meta"><span>Read <b>{asset.stance}</b></span><span>Model confidence <b>{Math.round(asset.confidence * 100)}%</b></span></div><section className="flow-market-references" aria-label={`${asset.ticker} instrument checks`}><b>Instrument checks</b><span>Tokenized spot · {asset.market.spotSymbol ?? `R${asset.ticker}USDT`} · {asset.market.spotLastPrice != null ? `$${asset.market.spotLastPrice.toLocaleString()}` : "No verified quote"}{asset.market.spotProviderTimestamp ? ` · ${new Date(asset.market.spotProviderTimestamp).toLocaleString()}` : " · freshness unverified"}</span><span>Stock perpetual · {asset.market.perpSymbol ?? `${asset.ticker}USDT`} · {asset.market.perpLastPrice != null ? `$${asset.market.perpLastPrice.toLocaleString()}` : "No verified quote"}{asset.market.perpProviderTimestamp ? ` · ${new Date(asset.market.perpProviderTimestamp).toLocaleString()}` : " · freshness unverified"} · derivative reference</span></section><p className="flow-confidence-note" role="note">{asset.confidenceCapped ? "Evidence is limited: the score is capped until two recent, distinct publishers support the assessment. " : ""}Model-reported score, not a calibrated probability.</p><QuoteFreshness market={asset.market} now={quoteNow} />{asset.evidenceBalance && (asset.evidenceBalance.supporting.length > 0 || asset.evidenceBalance.counter.length > 0) ? <section className="flow-evidence-balance" aria-label={`${asset.ticker} evidence balance`}><header><b>Evidence balance</b><span>Source-linked points only</span></header><div className="flow-evidence-columns">{([["Supports this reading", asset.evidenceBalance.supporting], ["Challenges this reading", asset.evidenceBalance.counter]] as const).map(([label, points]) => <section className="flow-evidence-side" key={label}><h4>{label}</h4>{points.length ? <ul>{points.map((point, index) => <li key={index}><p>{point.text}</p><div>{point.citedUrls.map((url) => { const source = asset.sources.find((item) => item.url === url); return source ? <a key={url} href={url} target="_blank" rel="noopener noreferrer">{source.publisher} ↗</a> : null; })}</div></li>)}</ul> : <p className="flow-evidence-empty">No source-linked points were established for this side.</p>}</section>)}</div></section> : null}{asset.gateReasons?.length ? <div className="flow-gates"><b>Decision gates</b><ul>{asset.gateReasons.map((reason, index) => <li key={index}>{reason}</li>)}</ul></div> : null}<details className="flow-sources"><summary>Inspect {asset.sources.length} {asset.sources.length === 1 ? "source" : "sources"}</summary>{asset.sources.length ? <ul>{asset.sources.map((source, index) => <li key={`${source.url}-${index}`}><span>{source.publisher}{source.publishedAt ? ` · ${new Date(source.publishedAt).toLocaleDateString()}` : ""}{source.via ? <em className="flow-source-via"> · {source.via}</em> : null}</span><a href={source.url} target="_blank" rel="noopener noreferrer">{source.title} ↗</a></li>)}</ul> : <p>No source links were returned; the trading gate remains closed.</p>}</details></article>)}</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recompute token quote freshness while displaying results

When a tokenized result remains open for more than two minutes, this badge continues to render the server-computed quoteStatus as fresh indefinitely, even though the component already advances quoteNow every second. This presents an expired spot quote as current; derive the status from spotProviderTimestamp and the current client time, or otherwise age the stored status before rendering.

Useful? React with 👍 / 👎.

{tradeableAssets.length ? <div className="flow-handoff" ref={handoffRef} tabIndex={-1} role="region" aria-label="Choose a trading review desk"><div className="flow-handoff-intro"><span className="flow-kicker">NEXT / YOUR DECISION</span><h2>Review the market before acting.</h2><p>The research gates found a scenario worth inspecting. Choose paper futures, or review a Bitget tokenized stock spot order in demo or live account mode. You can review both desks. Every order still requires your own approval and risk checks.</p></div><div className="flow-handoff-list">{tradeableAssets.map((asset) => <div className="flow-handoff-asset" key={asset.ticker}><div><b>{asset.ticker}</b><span>{asset.handoff?.reason}</span></div><div className="flow-handoff-actions">{asset.handoff?.destinations.flatMap((destination) => { const href = safeDeskLink(destination); if (!href) return []; if (destination.mode === "paper_futures") return [<Link key="paper" href={href} onClick={() => setMode("pro")}>Review paper futures <span>↗</span></Link>]; const demo = `${href}${href.includes("?") ? "&" : "?"}intent=demo`; const live = `${href}${href.includes("?") ? "&" : "?"}intent=live`; return [<Link key="demo" href={demo} onClick={() => setMode("pro")}>Review Bitget demo spot <span>↗</span></Link>, <Link key="live" href={live} onClick={() => setMode("pro")}>Review Bitget live spot <span>↗</span></Link>]; })}</div></div>)}</div><small>Research can be uncertain. A review link never places a trade or switches your account into live mode.{mode === "mini" ? " Trade reviews open the Pro workspace controls." : ""}</small></div> : <div className="flow-no-handoff"><span className="flow-kicker">RISK GATE / HOLD</span><b>No trading handoff for this read.</b><p>Review the sources and decision gates above. You can return when fresher or stronger evidence is available.</p><Link href="/markets">Explore the market map ↗</Link></div>}
</section> : null}
</div>;
Expand Down
Loading
Loading