Overview
Paths are relative to the dashboard: port 8080 on the server (fno-intel-dashboard.service); the examples call http://localhost:8080. Everything is read-only except the two POSTs — one adds research, the other sets a cookie. Nothing here places an order.
JSON comes back compact, with keys in alphabetical order and non-ASCII characters as \u escapes (— arrives as \u2014). The examples below are pretty-printed, and long values are shortened with …
- GET
/brain/api/searchMatching notes, signals and themes (JSON) - GET
/brain/api/nav.jsonIndex of notes, asset classes and themes (JSON) - GET
/brain/api/paste/<hash>One research note (JSON) - POST
/brain/api/themeSets the theme cookie (204) - GET
/brain/brief.mdToday’s brief (Markdown file) - GET
/brain/calendar.icsUpcoming catalysts (iCalendar file) - POST
/brain/pasteAdds a research note (HTML page) - GET
/healthzDatabase check (JSON)
GET/brain/api/search
Backs the search box at the top of every page. A case-insensitive match on research notes (source label and text), signals (the claim) and themes (the title).
Query parameters
| Name | Type | Notes |
|---|---|---|
q | string, required | At least 2 characters after trimming; anything shorter returns []. |
limit | integer, default 10 | Clamped to 1–50. The search box asks for 12. |
Response
A JSON array, best match first: the earliest match in the title or text, then the newest. The 50 newest matches of each kind are ranked. There is no error status — a search that fails returns [].
| Field | Meaning |
|---|---|
id | paste:<hash>, signal:<signal id> or cluster:<theme id> |
type | paste (a research note), signal or cluster (a theme) |
title | The note’s source label, the claim (cut at 90 characters) or the theme’s title |
subtitle | Note: the start of its text (100 characters). Signal: asset class and strength. Theme: asset class. |
url | The page that shows it |
hash | Notes and signals only: the research note to open in the evidence drawer (empty for a signal that did not come from a note) |
curl 'http://localhost:8080/brain/api/search?q=jackson+hole&limit=5'
[
{
"hash": "93b8e50fa86a5999",
"id": "signal:manual:93b8e50fa86a5999:2",
"subtitle": "Bonds and rates · strength 6",
"title": "Fed expected on hold through end-2026 despite hawkish Jackson Hole tone; real-rate suppre…",
"type": "signal",
"url": "/brain/paste/93b8e50fa86a5999"
},
{
"id": "cluster:406",
"subtitle": "Bonds and rates",
"title": "Fed expected on hold through end-2026 despite hawkish Jackson Hole tone; real-rate suppression scenario intact for gold",
"type": "cluster",
"url": "/brain/verdicts#fixed-income"
},
{
"hash": "93b8e50fa86a5999",
"id": "paste:93b8e50fa86a5999",
"subtitle": "Axis Bank gold report (dated September 3, 2026) is bullish on gold, forecasting a gradual rise towa…",
"title": "Axis Bank - Gold Outlook + Return/Drawdown Plan, Sep 2026",
"type": "paste",
"url": "/brain/paste/93b8e50fa86a5999"
}
]
GET/brain/api/paste/<hash>
One research note, in exactly the shape the evidence drawer and the full note page render.
Path parameter
| Name | Type | Notes |
|---|---|---|
hash | string | The note’s content hash: 16 hex characters, the start of the SHA-256 of its text. |
Response
200 with the note, never cached (Cache-Control: no-store). An unknown or malformed hash returns 404 {"error": "not found"}.
| Field | Meaning |
|---|---|
hash, url | Content hash and the full page |
title | Source label, with the title if one was given |
date, ist, age | Date added, IST timestamp and how long ago |
verdict, tone | Overall lean (BULLISH, BEARISH or MIXED) and its colour tone (bull, bear, mixed) |
signal_count, classes | Number of signals; asset classes covered, most signals first |
decision_md, decision_paragraphs | Claude’s decision summary, whole and split into paragraphs |
content_text, content_chars | The research exactly as added, and its length |
signals[] | text, asset_class, slug, delta (−5 to +5), delta_str, tone, confidence, strength (1–10) |
related[] | Themes the note feeds: title, verdict, tone, conviction (0–10), conviction_word, asset_class, href |
curl http://localhost:8080/brain/api/paste/93b8e50fa86a5999
{
"age": "31h ago",
"classes": ["Gold & commodities", "Bonds & rates"],
"content_chars": 3389,
"content_text": "Axis Bank gold report (dated September 3, 2026) is bullish on gold, …",
"date": "28 Sep 2026",
"decision_md": "Axis Bank (Sep-2026) is structurally bullish gold toward USD 6,000/oz …",
"decision_paragraphs": ["Axis Bank (Sep-2026) is structurally bullish gold …", …],
"hash": "93b8e50fa86a5999",
"ist": "2026-09-28 10:10",
"related": [
{
"asset_class": "Gold & commodities",
"conviction": 6.5,
"conviction_word": "high conviction",
"href": "/brain/verdicts#gold-commodities",
"title": "Axis Bank projects gold to USD 6,000/oz by ~Sep 2028; …",
"tone": "bull",
"verdict": "BULLISH"
},
…
],
"signal_count": 8,
"signals": [
{
"asset_class": "Gold & commodities",
"confidence": "medium",
"delta": 3.0,
"delta_str": "+3.0",
"slug": "gold-commodities",
"strength": 8.0,
"text": "Axis Bank projects gold to USD 6,000/oz by ~Sep 2028; …",
"tone": "bull"
},
…
],
"title": "Axis Bank - Gold Outlook + Return/Drawdown Plan, Sep 2026",
"tone": "bull",
"url": "/brain/paste/93b8e50fa86a5999",
"verdict": "BULLISH"
}
POST/brain/api/theme
Saves the light or dark theme in a cookie, so the next page is rendered in it without a flash. The theme button in the top bar calls it.
Request body
JSON, sent with Content-Type: application/json.
| Name | Type | Notes |
|---|---|---|
theme | string, required | "light" or "dark", in any case. |
Response
204 No Content, setting brain_theme for a year (SameSite=Lax). Any other value — or a body that isn’t JSON — returns 400 {"error": "theme must be 'light' or 'dark'"}.
curl -i -X POST http://localhost:8080/brain/api/theme \
-H 'Content-Type: application/json' \
-d '{"theme": "dark"}'
HTTP/1.1 204 NO CONTENT
Set-Cookie: brain_theme=dark; Expires=Wed, 29 Sep 2027 12:23:45 GMT; Max-Age=31536000; Path=/; SameSite=Lax
GET/brain/brief.md
Today’s decision brief as a Markdown file — the Export brief button. No parameters.
Response
text/markdown, downloaded as brain-brief-YYYY-MM-DD.md and never cached. It holds the regime and evidence agreement, the headline and summary, then This week, Actions to review, What changed (last 7 days), Market themes and Latest research.
curl -OJ http://localhost:8080/brain/brief.md
# Decision brief — Tuesday 29 Sep 2026
_Generated 2026-09-29 17:54 IST from the Investor Second Brain. Advisory only — nothing here places an order._
**Regime:** MIXED / DEFENSIVE · evidence agreement 60%
## Gold & commodities and Crypto are constructive; Indian equities, Bonds & rates and Global equities lean bearish.
Most one-sided: Gold & commodities (66% agreement across 6 sources). …
## This week
- New research: 2 (+1 vs prior week)
- Signals extracted: 16 (+8 vs prior week); 78 total across 7 asset classes
…
GET/brain/calendar.ics
Upcoming dated catalysts as an iCalendar file — the Sync calendar button on Catalysts. No parameters.
Response
text/calendar, downloaded as brain-catalysts.ics and never cached. One all-day event per upcoming catalyst: SUMMARY is the catalyst, DESCRIPTION its note tally and sources. Past and undated catalysts are left out; with nothing upcoming, the calendar has no events.
curl -OJ http://localhost:8080/brain/calendar.ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Investor Second Brain//Catalysts//EN
CALSCALE:GREGORIAN
X-WR-CALNAME:Brain catalysts
BEGIN:VEVENT
UID:fed-fomc-2026-10-28@brain
DTSTAMP:20260929T122400Z
DTSTART;VALUE=DATE:20261028
DTEND;VALUE=DATE:20261029
SUMMARY:Fed/FOMC
DESCRIPTION:2 notes · 1 bullish · 1 mixed — Axis Bank - India Macro Outlook\, Sep 2026\; Axis Bank - Gold Outlook + Return/Drawdown Plan\, Sep 2026
END:VEVENT
END:VCALENDAR
POST/brain/paste
Adds a research note. The Add research form posts here, and so does the Obsidian inbox watcher. Expect 30–60 seconds: two Claude calls plus the theme regrouping.
Form fields
Form-encoded — application/x-www-form-urlencoded or multipart/form-data. A JSON body is not read.
| Name | Required | Notes |
|---|---|---|
source | yes | Who the research is from. Saved as “source - title” when a title is given; the label is cut at 200 characters. |
title | no | The piece’s title. |
content | yes | The research text: 100–14,000 characters after trimming. |
Response
Always the Add research page as HTML with status 200, so read the message rather than the status code.
| Outcome | Message on the page |
|---|---|
| Added | Ingested N atomic signals from '…'. Themes refreshed. Obsidian vault updated. |
| Already added | This exact content was already ingested earlier (content_hash=…). All signals collided with prior rows. Nothing new inserted. |
| Rejected | Source label is required · Content too short (N chars) · Content too long (N chars). Nothing is sent to Claude. |
| Failed | LLM client init failed · LLM client unavailable · Claude scoring failed · Claude returned an error · Claude returned 0 atomic signals · DB insert failed |
| Partly done | Signals landed but theme clustering crashed: … Themes will refresh on the next daily aggregator run. |
The watcher (scripts/brain_obsidian_inbox_processor.py) treats any 200 as delivered and tells a duplicate or a failure apart by the phrases “already ingested earlier”, “DB insert failed” and “Claude scoring failed”, so keep that wording stable.
curl -X POST http://localhost:8080/brain/paste \
--data-urlencode 'source=Axis Bank' \
--data-urlencode 'title=Gold Outlook, Sep 2026' \
--data-urlencode 'content@note.txt'
Ingested 8 atomic signals from 'Axis Bank - Gold Outlook, Sep 2026'. Themes refreshed. Obsidian vault updated.
GET/healthz
A liveness check for monitors: runs SELECT 1 against the database. It sits at the root, not under /brain.
Response
200 {"ok": true}, or 500 {"error": "…", "ok": false} when the database can’t be reached.
curl http://localhost:8080/healthz
{"ok":true}
Not available over HTTP
- JSON ingestion.
POST /brain/pastereads form fields only; a JSON body arrives without a source and is rejected. - Deleting a note. There is no route; removing research is a database operation.
- Re-running an analysis. There is no route, and posting the same text again is treated as a duplicate.