Status: Draft v1 — pending PRD approval
Last updated: 2026-05-18
Sibling docs: prd.md (requirements) · design-system.md (tokens/components) · research-apple-vc-shared.md (shell reuse)
Stack: 100% Cloudflare (Workers, D1, R2, KV, Queues, Workers AI, Cron)
web/intellectia.id/worker-cf/
intellectia-db)CACHE)intellectia-r2) — OG images, TikTok assets, weekly PDFsai-jobs, wa-blast) — async heavy AI workai-router (OpenRouter ↔ CF Workers AI ↔ kie.ai)worker-cf/
├── src/
│ ├── index.ts # Router
│ ├── lib/
│ │ ├── ai-router.ts # vendored from pro/ai-router/router.js
│ │ ├── idx.ts # IDX data fetcher (quotes, fundamentals)
│ │ ├── crypto.ts # indodax/tokocrypto IDR pairs
│ │ ├── reksadana.ts # Bareksa scrape / KSEI feed
│ │ ├── kie_image.ts # z-image hero gen (vendored from nujum.id)
│ │ ├── disclaimer.ts # legal blurb injector
│ │ ├── qris.ts # Mayar integration
│ │ ├── wa.ts # WhatsApp Cloud API
│ │ ├── telegram.ts # Telegram Bot API
│ │ ├── ta.ts # technical analysis (RSI/MACD/SMA)
│ │ ├── patterns.ts # chart pattern detection
│ │ └── auth.ts # session/JWT, tier gating middleware
│ ├── views/
│ │ ├── layout.ts # Shared shell
│ │ ├── home.ts # Briefing harian + top picks
│ │ ├── saham.ts # /saham/<ticker> per-ticker analysis
│ │ ├── screener.ts # NLP screener UI
│ │ ├── tanya.ts # AI agent chat
│ │ ├── pilihan.ts # Daily AI picks
│ │ ├── swing.ts # SwingID signals
│ │ ├── earnings.ts # Earnings prediction
│ │ ├── crypto.ts # Crypto radar
│ │ ├── reksadana.ts # Reksa dana screener
│ │ ├── sbn.ts # SBN/ORI tracker
│ │ ├── kalender.ts # Calendars (ekonomi + earnings)
│ │ ├── ipo.ts # IPO center
│ │ ├── komunitas.ts # Discussion threads
│ │ ├── edukasi.ts # Articles (pSEO)
│ │ ├── glossary.ts # Per-term glossary pages
│ │ ├── syariah.ts # JII70/ISSI filter
│ │ ├── pajak.ts # Tax helper
│ │ ├── portfolio.ts # Manual/CSV portfolio
│ │ ├── pricing.ts # Tiers + QRIS flow
│ │ ├── auth.ts # Login/signup
│ │ └── legal.ts # T&C, privacy, disclaimer
│ ├── api/
│ │ ├── quotes.ts # /api/quotes/<ticker>
│ │ ├── chat.ts # /api/chat (AI agent SSE)
│ │ ├── screener.ts # /api/screener (NLP → SQL)
│ │ ├── alerts.ts # /api/alerts CRUD
│ │ ├── watchlist.ts # CRUD
│ │ ├── portfolio.ts # CRUD + CSV import
│ │ ├── comments.ts # /api/comments CRUD
│ │ └── webhook-mayar.ts # QRIS payment confirm
│ ├── cron/
│ │ ├── ingest-idx.ts # 5-min quote pull during market hours
│ │ ├── ingest-crypto.ts # 5-min crypto pull
│ │ ├── ingest-reksadana.ts # daily NAV pull
│ │ ├── ingest-macro.ts # daily BI/BPS pull
│ │ ├── daily-picks.ts # 07:30 WIB AI top-5 generation
│ │ ├── briefing.ts # 08:30 + 16:30 WIB market brief
│ │ ├── news-sentiment.ts # hourly news scrape + AI sentiment
│ │ ├── earnings-scan.ts # daily KSEI disclosure scan
│ │ ├── alerts-eval.ts # every 5min, fire alerts
│ │ ├── wa-blast.ts # WA Premium daily push
│ │ ├── tiktok-gen.ts # daily TikTok short auto-gen
│ │ ├── indexnow.ts # SEO ping (per nujum.id pattern)
│ │ ├── sitemap.ts # daily sitemap rebuild
│ │ └── tier-expiry.ts # daily auto-downgrade
│ ├── queue/
│ │ ├── ai-jobs.ts # async AI heavy jobs (per-ticker analysis)
│ │ └── wa-blast.ts # WA send queue (rate-limited)
│ └── widget.js # embeddable "AI analisis saham" widget
├── public/
│ ├── icon-192.png
│ ├── icon-512.png
│ ├── manifest.webmanifest
│ └── robots.txt
├── schema.sql # D1 schema
├── seed/
│ ├── 01_tickers.sql # IDX 900-list
│ ├── 02_sectors.sql # GICS sectors
│ ├── 03_glossary.sql # Glossary terms
│ ├── 04_edukasi.sql # Seed pSEO articles
│ └── 05_syariah.sql # JII70/ISSI memberships
├── scripts/
│ ├── pull-idx-tickers.js # one-time IDX ticker list
│ └── pull-syariah.js # OJK Syariah list pull
├── package.json
├── tsconfig.json
└── wrangler.toml
-- Users & auth
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE,
wa_phone TEXT UNIQUE,
tg_chat_id TEXT UNIQUE,
name TEXT,
tier TEXT NOT NULL DEFAULT 'free', -- free|wa|pro|expert
qris_expires_at INTEGER,
prompts_used_today INTEGER DEFAULT 0,
prompts_reset_at INTEGER,
created_at INTEGER NOT NULL
);
-- Universe
CREATE TABLE tickers (
symbol TEXT PRIMARY KEY,
name TEXT NOT NULL,
sector TEXT,
sub_sector TEXT,
ihsg_weight REAL,
is_syariah INTEGER DEFAULT 0,
is_lq45 INTEGER DEFAULT 0,
is_idx30 INTEGER DEFAULT 0,
is_jii70 INTEGER DEFAULT 0,
mkt_cap REAL,
listed_at TEXT
);
CREATE INDEX idx_tickers_sector ON tickers(sector);
CREATE INDEX idx_tickers_lq45 ON tickers(is_lq45);
-- Price data
CREATE TABLE quotes_daily (
symbol TEXT NOT NULL,
date TEXT NOT NULL,
open REAL, high REAL, low REAL, close REAL,
volume INTEGER,
foreign_buy REAL, foreign_sell REAL,
PRIMARY KEY (symbol, date)
);
CREATE INDEX idx_quotes_daily_date ON quotes_daily(date);
CREATE TABLE quotes_intraday (
symbol TEXT NOT NULL,
ts INTEGER NOT NULL,
price REAL,
volume INTEGER,
PRIMARY KEY (symbol, ts)
);
-- TTL: prune > 5 days via cron
-- Fundamentals
CREATE TABLE fundamentals (
symbol TEXT NOT NULL,
fy_q TEXT NOT NULL, -- '2026Q1'
revenue REAL, net_income REAL, eps REAL,
per REAL, pbv REAL, roe REAL, der REAL,
dividend_yield REAL,
PRIMARY KEY (symbol, fy_q)
);
-- News
CREATE TABLE news (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT, url TEXT UNIQUE,
title TEXT, body TEXT,
symbols TEXT, -- JSON array of tickers
sentiment REAL, -- -1..+1
ts INTEGER NOT NULL
);
CREATE INDEX idx_news_ts ON news(ts);
-- AI cache
CREATE TABLE ai_analysis (
symbol TEXT NOT NULL,
kind TEXT NOT NULL, -- fundamental|teknikal|sentimen|prediksi
payload_json TEXT,
generated_at INTEGER NOT NULL,
ttl INTEGER NOT NULL,
PRIMARY KEY (symbol, kind)
);
-- Picks & signals
CREATE TABLE picks_daily (
date TEXT NOT NULL,
rank INTEGER NOT NULL,
symbol TEXT NOT NULL,
entry REAL, target REAL, stop REAL,
reasoning_md TEXT,
PRIMARY KEY (date, rank)
);
CREATE TABLE signals_swing (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
side TEXT NOT NULL, -- long|short
entry REAL, target REAL, stop REAL,
conviction REAL,
opened_at INTEGER NOT NULL,
closed_at INTEGER, pnl REAL
);
CREATE TABLE earnings_predictions (
symbol TEXT NOT NULL,
fy_q TEXT NOT NULL,
beat_prob REAL,
eps_est REAL,
sentiment REAL,
generated_at INTEGER,
PRIMARY KEY (symbol, fy_q)
);
-- User state
CREATE TABLE watchlists (
user_id INTEGER NOT NULL,
symbol TEXT NOT NULL,
added_at INTEGER NOT NULL,
PRIMARY KEY (user_id, symbol)
);
CREATE TABLE alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
symbol TEXT NOT NULL,
kind TEXT NOT NULL, -- price_above|price_below|pattern|news
threshold REAL,
channel TEXT NOT NULL, -- wa|tg|email
active INTEGER DEFAULT 1,
last_fired_at INTEGER
);
CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticker TEXT NOT NULL,
user_id INTEGER NOT NULL,
body TEXT NOT NULL,
parent_id INTEGER,
ts INTEGER NOT NULL
);
CREATE INDEX idx_comments_ticker ON comments(ticker);
CREATE TABLE portfolios (
user_id INTEGER NOT NULL,
symbol TEXT NOT NULL,
qty REAL NOT NULL,
avg_cost REAL NOT NULL,
added_at INTEGER NOT NULL,
PRIMARY KEY (user_id, symbol)
);
-- Usage metering
CREATE TABLE chat_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
prompt TEXT,
response TEXT,
tokens INTEGER,
cost REAL,
ts INTEGER NOT NULL
);
-- Payments
CREATE TABLE payments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
mayar_id TEXT UNIQUE,
amount REAL,
tier TEXT,
status TEXT, -- pending|paid|failed|expired
paid_at INTEGER
);
-- Crypto
CREATE TABLE crypto_tokens (
symbol TEXT PRIMARY KEY, -- BTC, ETH, IDX-listed pair
name TEXT, is_bappebti INTEGER DEFAULT 0
);
CREATE TABLE crypto_quotes (
symbol TEXT NOT NULL,
ts INTEGER NOT NULL,
price_idr REAL, volume_idr REAL,
PRIMARY KEY (symbol, ts)
);
-- Reksa dana
CREATE TABLE reksadana (
code TEXT PRIMARY KEY,
name TEXT, mi TEXT, -- Manajer Investasi
kategori TEXT, -- saham|pendapatan_tetap|campuran|pasar_uang
aum REAL
);
CREATE TABLE reksadana_nav (
code TEXT NOT NULL,
date TEXT NOT NULL,
nav REAL,
PRIMARY KEY (code, date)
);
-- Macro
CREATE TABLE macro_indicators (
series TEXT NOT NULL, -- bi_rate|inflasi|gdp
date TEXT NOT NULL,
value REAL,
PRIMARY KEY (series, date)
);
-- Calendars
CREATE TABLE calendar_ekonomi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event TEXT, ts INTEGER, impact TEXT
);
CREATE TABLE calendar_earnings (
symbol TEXT NOT NULL,
fy_q TEXT NOT NULL,
disclosure_date TEXT,
PRIMARY KEY (symbol, fy_q)
);
-- IPO
CREATE TABLE ipos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT, name TEXT, sector TEXT,
price_range TEXT,
bookbuilding_start TEXT, bookbuilding_end TEXT,
listing_date TEXT,
prospektus_url TEXT, ai_summary_md TEXT
);
-- Glossary & edukasi (pSEO)
CREATE TABLE glossary (
slug TEXT PRIMARY KEY,
term TEXT, body_md TEXT, updated_at INTEGER
);
CREATE TABLE edukasi (
slug TEXT PRIMARY KEY,
title TEXT, body_md TEXT,
tags TEXT, og_image TEXT, updated_at INTEGER
);
Var-limit notes: D1 has ~100 SQL var limit (gotcha). Chunk bulk inserts to ≤90 rows per statement.
CACHE)quote:<symbol> — TTL 5 min (during market hours), 1 day (off-hours)page:saham:<symbol> — TTL 5 minpage:home — TTL 60 sscreener:<hash> — TTL 30 minnews:<symbol> — TTL 15 minintellectia-r2)og/<symbol>.webp — per-ticker OG card (z-image, xmoj-resized)og/home/<date>.webp — daily home OGtiktok/<date>.mp4 — daily auto-gen shortpdf/<user_id>/<week>.pdf — Expert weekly reportVendor src/lib/ai-router.ts from /home/ucok/pro/ai-router/router.js. Per task:
| Task | Model | Notes |
|---|---|---|
| Bulk per-ticker analysis (~900/day) | DeepSeek v3.2 via OR | Cheapest reasoning |
| Tanya Saham conversational | gpt-5-mini via OR | Tools, SSE streaming |
| News sentiment classification | DeepSeek v3.2 (batch) | One-shot per article |
| Screener NLP → SQL | gpt-5-mini | Constrained to safe SELECTs |
| Embeddings (news, screener) | CF Workers AI @cf/baai/bge-base-en-v1.5 |
Free tier |
| Hero card images | kie.ai z-image | R2-cached, xmoj-resized |
| TikTok narration TTS | ElevenLabs ID voice (paid) or Piper neural | Budget-gated |
Falls back to CF Workers AI when OPENROUTER_API_KEY missing.
Heavy AI work never blocks request — always ctx.waitUntil or push to ai-jobs queue. Per kie_image.ts pattern from nujum.id.
| Cron | Schedule | Job |
|---|---|---|
ingest-idx |
*/5 02-09 * * 1-5 UTC |
5-min IDX quote pull (09:00–16:30 WIB) |
ingest-crypto |
*/5 * * * * |
24/7 crypto pull |
ingest-reksadana |
0 22 * * * UTC = 05:00 WIB |
Daily NAV pull |
ingest-macro |
0 1 * * * UTC = 08:00 WIB |
BI/BPS daily |
news-sentiment |
15 * * * * |
Hourly news scrape + sentiment |
earnings-scan |
0 1 * * * UTC |
Daily KSEI disclosure scan |
daily-picks |
30 0 * * 1-5 UTC = 07:30 WIB |
Top-5 AI picks |
briefing |
30 1 * * 1-5 UTC = 08:30 WIB & 30 9 * * 1-5 UTC = 16:30 WIB |
Market brief |
wa-blast |
0 1 * * 1-5 UTC = 08:00 WIB |
Premium WA push |
alerts-eval |
*/5 02-09 * * 1-5 UTC |
Fire alerts |
tiktok-gen |
0 10 * * * UTC = 17:00 WIB |
Daily short |
indexnow |
0 22 * * * UTC |
SEO ping |
sitemap |
0 22 * * * UTC |
Rebuild |
tier-expiry |
0 0 * * * UTC |
Auto-downgrade |
| Source | Use | Notes |
|---|---|---|
IDX endpoints (idx.co.id) |
quotes, fundamentals | Scrape JSON endpoints; Yahoo Finance ^JKSE as fallback |
| KSEI disclosures | insider/earnings | Scrape ksei.co.id schedules |
| OJK | regulatory filings | RSS + scrape |
| Indodax/Tokocrypto public API | crypto IDR | Free public REST |
| Bareksa scrape | reksa dana NAV | TOS check; may need partner deal |
| Bank Indonesia | BI rate, inflasi | bi.go.id JSON |
| BPS | macro indicators | bps.go.id |
| Kontan / Bisnis / CNBC ID RSS | news | RSS aggregation |
| Stockbit Stream (public) | sentiment seed | Careful scrape |
All scraped data cached in D1+KV; never re-fetch on request.
Mirror nujum.id playbook:
/saham/BBCA × ~900 = 900 evergreen pages, daily-fresh content blocks./sektor/perbankan, /sektor/teknologi.intellectia.id to xmoj whitelist).ucok-intellectia with 10–12 child issuesintellectia.id availability via domain-admin skilladd-domain skillweb/intellectia.id/worker-cf/ skeletonwrangler.toml; D1 create (intellectia-db); KV create; R2 create; Queues createschema.sql migration; seed tickers (IDX 900-list)ai-router.ts + kie_image.tswidget.jsgoogle-search-console skill)seo-boost skill)curl https://intellectia.id/saham/BBCA returns full HTML with disclaimer, fundamentals, AI analysis, OG image./api/chat SSE streams Bahasa Indonesia analysis for $BBCA RSI.wrangler tail shows no errors during market hours./home/ucok/pro/ai-router/router.js — AI routing (vendor in)/home/ucok/web/nujum.id/worker-cf/src/lib/kie_image.ts — non-blocking R2-cached image gen/home/ucok/web/nujum.id/worker-cf/src/lib/ — deterministic+AI hybrid pattern/home/ucok/web/pranala.org/worker-cf/src/lib/indexnow.ts — IndexNow pattern/home/ucok/web/pranala.org/worker-cf/src/cron/sitemap.ts — sitemap pattern/home/ucok/web/xmoj.com — image CDN (whitelist intellectia.id)/home/ucok/web/apple.vc/worker-cf/src/views/ — shared dashboard/admin views (research candidate, see §13)/home/ucok/pro/docs/openrouter-models.md — model defaultsmemory/cf_deployment_policy.md — CF deploy rulesmemory/cf_d1_cost_optimization.md — edge cache + bot gate + sampled writessrc/lib/disclaimer.ts exports bannerHTML() and footerHTML(); every view must include both./rekomendasi|pasti|dijamin/gi → "analisis" / "berpotensi" / "berpeluang"./api/chat response always appended with disclaimer.| Risk | Mitigation |
|---|---|
| OJK exposure on "rekomendasi" framing | Disclaimer-heavy, regex strip wording, language-policy in AI prompts. Legal review pre-launch. |
| IDX scraping ToS | Yahoo Finance fallback; rate-limit + UA rotation; budget for licensed feed if scaling. |
| Real-time quote license cost | v1 = 15-min delayed (matches Stockbit free). |
| Workers CPU-time limit on backtests | Push to Queues; chunked workers; precompute factor scores. |
| WA Cloud API per-session cost | Price into Rp 49k margin; batch sends; opt-in only. |
| TikTok auto-gen robotic voice | Try Piper neural ID first; ElevenLabs if budget allows. |
| Stockbit community moat | Win on AI depth, multi-asset, WA-native delivery. |
| D1 var limit (~100) on bulk inserts | Chunk to ≤90 rows; sleep 5s between executes (per gotcha). |
| CF Pages JS cache 30d (gotcha) | We're Worker-only; widget.js?v=DATE versioning. |
| Custom domain edge cache 1h after deploy | Purge zone cache post-deploy. |
web/apple.vc/ — apple.vc has admin/dashboard/leaderboard/profile/community/projects views that may be reusable as drop-ins for intellectia.id's dashboard, leaderboard, komunitas, and portfolio pages. Pending analysis — see followup research note.This domain MUST operate within these constraints — no exceptions:
If the plan above describes any flow that violates these constraints, treat the plan as ASPIRATIONAL only and rework before building. The constraint trifecta wins.
Ask AI to research, improve, or generate content.
Try: "Research competitors for this niche"