From f401155cfc255f2813e680ba7caa4cb3935be022 Mon Sep 17 00:00:00 2001 From: Asiwaju Date: Wed, 21 Oct 2026 09:48:27 +0100 Subject: [PATCH 1/2] Make tokenized-market review findings explicit --- app/api/research/agent-flow/route.ts | 4 ++-- app/flow/flow.css | 1 + app/ui/agent-flow-desk.tsx | 4 ++-- lib/agent-flow-policy.ts | 14 ++++++++++++++ tests/agent-flow-policy.test.mjs | 14 +++++++++++++- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/api/research/agent-flow/route.ts b/app/api/research/agent-flow/route.ts index deebe2c0..5f4f741d 100644 --- a/app/api/research/agent-flow/route.ts +++ b/app/api/research/agent-flow/route.ts @@ -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"; @@ -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 }) } : {}), 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, diff --git a/app/flow/flow.css b/app/flow/flow.css index cf3e4822..7594b913 100644 --- a/app/flow/flow.css +++ b/app/flow/flow.css @@ -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)} diff --git a/app/ui/agent-flow-desk.tsx b/app/ui/agent-flow-desk.tsx index 0185339d..0aaf649e 100644 --- a/app/ui/agent-flow-desk.tsx +++ b/app/ui/agent-flow-desk.tsx @@ -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 }; @@ -176,7 +176,7 @@ export default function AgentFlowDesk({ initialQuestion, evaluatorMode = false } {error ?
Research paused{error}{error.toLowerCase().includes("sign in") ? Sign in ↗ : }
: null} {result ?
RESEARCH / {result.assets.length} {result.assets.length === 1 ? "ISSUER" : "ISSUERS"}

One question. Separate decisions.

Issuer coverage and token-market conditions are shown separately. Review the source links and quote timestamp before considering any next step.

-
{result.assets.map((asset) =>
{asset.ticker} / {asset.issuer}

{asset.issuer}

{asset.tradeable ? "REVIEW AVAILABLE" : "NO TRADE HANDOFF"}

{asset.summary}

Read {asset.stance}Model confidence {Math.round(asset.confidence * 100)}%
Instrument checksTokenized 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"}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

{asset.confidenceCapped ? "Evidence is limited: the score is capped until two recent, distinct publishers support the assessment. " : ""}Model-reported score, not a calibrated probability.

{asset.evidenceBalance && (asset.evidenceBalance.supporting.length > 0 || asset.evidenceBalance.counter.length > 0) ?
Evidence balanceSource-linked points only
{([["Supports this reading", asset.evidenceBalance.supporting], ["Challenges this reading", asset.evidenceBalance.counter]] as const).map(([label, points]) =>

{label}

{points.length ?
    {points.map((point, index) =>
  • {point.text}

    {point.citedUrls.map((url) => { const source = asset.sources.find((item) => item.url === url); return source ? {source.publisher} ↗ : null; })}
  • )}
:

No source-linked points were established for this side.

}
)}
: null}{asset.gateReasons?.length ?
Decision gates
    {asset.gateReasons.map((reason, index) =>
  • {reason}
  • )}
: null}
Inspect {asset.sources.length} {asset.sources.length === 1 ? "source" : "sources"}{asset.sources.length ?
    {asset.sources.map((source, index) =>
  • {source.publisher}{source.publishedAt ? ` · ${new Date(source.publishedAt).toLocaleDateString()}` : ""}{source.via ? · {source.via} : null}{source.title} ↗
  • )}
:

No source links were returned; the trading gate remains closed.

}
)}
+
{result.assets.map((asset) =>
{asset.ticker} / {asset.issuer}

{asset.issuer}

{asset.tradeable ? "REVIEW AVAILABLE" : "NO TRADE HANDOFF"}

{asset.summary}

{asset.tokenizedMarketReview ?
R TOKEN / MARKET FINDING{asset.tokenizedMarketReview.verdict}Token quote freshness: {asset.tokenizedMarketReview.quoteStatus}
    {asset.tokenizedMarketReview.limitations.map((limitation) =>
  • {limitation}
  • )}
: null}
Read {asset.stance}Model confidence {Math.round(asset.confidence * 100)}%
Instrument checksTokenized 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"}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

{asset.confidenceCapped ? "Evidence is limited: the score is capped until two recent, distinct publishers support the assessment. " : ""}Model-reported score, not a calibrated probability.

{asset.evidenceBalance && (asset.evidenceBalance.supporting.length > 0 || asset.evidenceBalance.counter.length > 0) ?
Evidence balanceSource-linked points only
{([["Supports this reading", asset.evidenceBalance.supporting], ["Challenges this reading", asset.evidenceBalance.counter]] as const).map(([label, points]) =>

{label}

{points.length ?
    {points.map((point, index) =>
  • {point.text}

    {point.citedUrls.map((url) => { const source = asset.sources.find((item) => item.url === url); return source ? {source.publisher} ↗ : null; })}
  • )}
:

No source-linked points were established for this side.

}
)}
: null}{asset.gateReasons?.length ?
Decision gates
    {asset.gateReasons.map((reason, index) =>
  • {reason}
  • )}
: null}
Inspect {asset.sources.length} {asset.sources.length === 1 ? "source" : "sources"}{asset.sources.length ?
    {asset.sources.map((source, index) =>
  • {source.publisher}{source.publishedAt ? ` · ${new Date(source.publishedAt).toLocaleDateString()}` : ""}{source.via ? · {source.via} : null}{source.title} ↗
  • )}
:

No source links were returned; the trading gate remains closed.

}
)}
{tradeableAssets.length ?
NEXT / YOUR DECISION

Review the market before acting.

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.

{tradeableAssets.map((asset) =>
{asset.ticker}{asset.handoff?.reason}
{asset.handoff?.destinations.flatMap((destination) => { const href = safeDeskLink(destination); if (!href) return []; if (destination.mode === "paper_futures") return [ setMode("pro")}>Review paper futures ↗]; const demo = `${href}${href.includes("?") ? "&" : "?"}intent=demo`; const live = `${href}${href.includes("?") ? "&" : "?"}intent=live`; return [ setMode("pro")}>Review Bitget demo spot ↗, setMode("pro")}>Review Bitget live spot ↗]; })}
)}
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." : ""}
:
RISK GATE / HOLDNo trading handoff for this read.

Review the sources and decision gates above. You can return when fresher or stronger evidence is available.

Explore the market map ↗
}
: null} ; diff --git a/lib/agent-flow-policy.ts b/lib/agent-flow-policy.ts index 81184220..12b3bb03 100644 --- a/lib/agent-flow-policy.ts +++ b/lib/agent-flow-policy.ts @@ -25,6 +25,20 @@ export function sourceOnlyAssessment(input: { ticker: string; headlineCount: num return { summary, stance: "unclear", confidence: 0, citedUrls: [], supportingEvidence: [], counterEvidence: [], risks: ["AI synthesis is unavailable; retrieved headlines are discovery references only.", ...(input.tokenizedMarketQuestion ? ["Issuer coverage does not establish rToken liquidity, spread, or premium/discount."] : [])], nextCheck: input.tokenizedMarketQuestion ? "Verify the rToken's live availability, quote freshness, liquidity, spread, and premium or discount from instrument-specific sources." : "Review issuer filings and completed candles." }; } +/** Deterministic instrument-specific conclusion, independent of model prose. */ +export function tokenizedMarketReview(input: { ticker: string; spotSymbol: string | null; spotPrice: number | null; spotTimestamp: number | null; now: number }) { + const quoteAvailable = Boolean(input.spotSymbol && typeof input.spotPrice === "number" && Number.isFinite(input.spotPrice) && input.spotPrice > 0); + const timestampKnown = typeof input.spotTimestamp === "number" && Number.isFinite(input.spotTimestamp) && input.spotTimestamp > 0 && input.spotTimestamp <= input.now + 60_000; + const quoteFresh = Boolean(quoteAvailable && timestampKnown && input.now - input.spotTimestamp! <= 120_000); + return { + verdict: !quoteAvailable + ? `Not enough evidence for an r${input.ticker} market review: no verified tokenized-spot quote was returned.` + : `Worth a closer due-diligence review; the available evidence does not establish r${input.ticker} tradability.`, + quoteStatus: !quoteAvailable ? "unavailable" as const : !timestampKnown ? "unverified" as const : quoteFresh ? "fresh" as const : "stale" as const, + limitations: ["Order-book liquidity and depth were not measured.", "Bid–ask spread was not measured.", "Premium or discount to the underlying was not measured."], + }; +} + const aliases: Record = { NVDA: ["NVIDIA"], TSLA: ["TESLA"], AAPL: ["APPLE"], AMD: ["ADVANCED MICRO DEVICES"], MSFT: ["MICROSOFT"], META: ["META PLATFORMS"], AMZN: ["AMAZON"], diff --git a/tests/agent-flow-policy.test.mjs b/tests/agent-flow-policy.test.mjs index 0a82bde3..fe8f6eb3 100644 --- a/tests/agent-flow-policy.test.mjs +++ b/tests/agent-flow-policy.test.mjs @@ -1,6 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { extractFlowTickers, gateFlowTrade, normalizeFlowAssessment, normalizeFlowAssessments, sourceOnlyAssessment } from "../lib/agent-flow-policy.ts"; +import { extractFlowTickers, gateFlowTrade, normalizeFlowAssessment, normalizeFlowAssessments, sourceOnlyAssessment, tokenizedMarketReview } from "../lib/agent-flow-policy.ts"; const now = Date.now(); const sources = [ @@ -21,6 +21,18 @@ test("source-only rToken research distinguishes issuer coverage from token-marke assert.equal(result.confidence, 0); }); +test("tokenized market verdict separates due diligence from trading merit and reports quote age", () => { + const fresh = tokenizedMarketReview({ ticker: "NVDA", spotSymbol: "RNVDAUSDT", spotPrice: 220, spotTimestamp: now - 30_000, now }); + assert.match(fresh.verdict, /Worth a closer due-diligence review/); + assert.match(fresh.verdict, /does not establish rNVDA tradability/); + assert.equal(fresh.quoteStatus, "fresh"); + const stale = tokenizedMarketReview({ ticker: "NVDA", spotSymbol: "RNVDAUSDT", spotPrice: 220, spotTimestamp: now - 180_000, now }); + assert.equal(stale.quoteStatus, "stale"); + const absent = tokenizedMarketReview({ ticker: "NVDA", spotSymbol: null, spotPrice: null, spotTimestamp: null, now }); + assert.match(absent.verdict, /no verified tokenized-spot quote/); + assert.equal(absent.quoteStatus, "unavailable"); +}); + test("the coordinator recognizes only supported US stock underlyings", () => { assert.deepEqual(extractFlowTickers("What do you think about NVDA and TSLA?", ["NVDAUSDT", "TSLAUSDT", "AMDUSDT"]), ["NVDA", "TSLA"]); assert.deepEqual(extractFlowTickers("rNVDAUSDT and NVIDIA", ["NVDAUSDT"]), ["NVDA"]); From b52ab0ec8f0da04dcdc883cdf0a4c9d5b9526ca1 Mon Sep 17 00:00:00 2001 From: Asiwaju Date: Wed, 21 Oct 2026 09:51:42 +0100 Subject: [PATCH 2/2] Fix Overview token pulse replay persistence --- app/api/research/token-pulse/route.ts | 2 +- tests/token-pulse-replay-save.test.mjs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/token-pulse-replay-save.test.mjs diff --git a/app/api/research/token-pulse/route.ts b/app/api/research/token-pulse/route.ts index a85d14f7..8c5abe70 100644 --- a/app/api/research/token-pulse/route.ts +++ b/app/api/research/token-pulse/route.ts @@ -65,7 +65,7 @@ async function savePulseReplaySource(db: Awaited 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; } diff --git a/tests/token-pulse-replay-save.test.mjs b/tests/token-pulse-replay-save.test.mjs new file mode 100644 index 00000000..862d14a7 --- /dev/null +++ b/tests/token-pulse-replay-save.test.mjs @@ -0,0 +1,14 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; + +const read = (path) => readFile(new URL(path, import.meta.url), "utf8"); + +test("Overview token pulse stores news and community items using an allowed research source type", async () => { + const route = await read("../app/api/research/token-pulse/route.ts"); + const schema = await read("../supabase/migrations/20261004104708_initial_tidelight_research_schema.sql"); + assert.match(schema, /source_type text not null default 'web' check \(source_type in \('web','market_data','filing','user_note'\)\)/); + assert.match(route, /research_sources"\)\.upsert\(items\.map\(item => \(\{[\s\S]*?source_type: "web"/); + assert.doesNotMatch(route, /source_type: item\.channel\.toLowerCase\(\)/); + assert.match(route, /researchRunId/); +});