Phase 1 backend/data layer for a Market Discovery & Consensus system.
X API → Val Town collector → normalized storage (SQLite) → Claude → Market Discovery & Consensus
This val is the first two boxes in that diagram (collector + storage). It does not run a schedule yet, and it does not do any sentiment/thesis analysis — both are intentionally deferred (see "Phase 2" below).
config.ts): @aleabitoreddit, @jukan05, @octopusycc,
@zephyr_z9, @KawzInvests, @michaelsikand, @ren_stocks, @unusual_whales,
@StockMKTNewz, @DJTRadar.GET /2/users/by).GET /2/users/:id/tweets), including original posts,
replies, quotes, and reposts.posts table), classifying post_type and
extracting cashtag tickers mechanically.The X API requires an app-only Bearer Token to read tweets. This project reads it from an environment variable and never stores or accepts it any other way — it is never hard-coded, never logged, and was never pasted into this chat.
👉 Add X_BEARER_TOKEN here: https://www.val.town/x/efrenlin/x-trader-consensus/environment-variables?key=X_BEARER_TOKEN
Steps to get the token:
X_BEARER_TOKEN at the link above. Nowhere else.Cost note: the X API no longer has a simple free read tier — it now bills per-resource on a pay-per-use credit basis (roughly $0.01 per user lookup, ~$0.005 per tweet read, at current published rates). Reading 10 accounts × ~25 posts every few hours has a small but non-zero ongoing cost, so make sure the developer account backing this token has billing/ credits enabled, or requests will fail with an auth/payment error. Confirm current pricing in the X Developer Portal before relying on these numbers — they change.
config.ts Tracked account handles + tunables. No credentials.
lib/schema.ts SQLite schema (idempotent CREATE TABLE IF NOT EXISTS)
lib/xapi.ts X API v2 client (resolveUsernames, fetchUserTweets)
lib/normalize.ts Mechanical normalization: post_type, ticker extraction
lib/collector.ts Orchestrates one full collection pass across all accounts
main.ts (http) Manual trigger + inspection routes (see below)
Base URL: https://efrenlin--b9b4c5ceaa0011f1b6d11607ee4eb77e.web.val.run
GET /health — status: is the token configured, which accounts are resolved, how many
posts are stored, how many are still awaiting analysis.GET /run-collector — runs one collection pass across all 10 accounts. Requires
X_BEARER_TOKEN to be set; otherwise returns 400 with a clear message.GET /posts?account=<username>&limit=<n> — inspect normalized posts (JSON), most recent
first. Omit account to see all tracked accounts.GET /runs — history of the last 20 collector runs (accounts processed, posts ingested,
any per-account errors).Per-post fields, matching the requested schema:
| Field | Populated by | Notes |
|---|---|---|
account, username | collector | display name / @handle |
post_id, posted_at, original_url | collector | |
post_type | collector | original | reply | quote | repost, from the API's referenced_tweets |
referenced_post_id | collector | underlying tweet id for reply/quote/repost — the join key for dedup |
text | collector | raw post text |
mentioned_tickers | collector | mechanical $CASHTAG regex + API cashtag entities — JSON array |
engagement_metrics | collector | retweet/reply/like/quote/impression counts — JSON object |
ingested_at | collector | when this row was written |
explicit_stance | Claude (Phase 2) | Bullish | Bearish | Unclear — left NULL until analyzed |
evidence | Claude (Phase 2) | supporting excerpt for the stance |
thesis, catalyst, time_horizon | Claude (Phase 2) | left NULL until analyzed |
position_disclosure | Claude (Phase 2) | only ever filled when the post explicitly states one |
analyzed_at | Claude (Phase 2) | timestamp once a row has been analyzed |
The spec is explicit: "Do not infer a position unless explicitly supported by the post,"
and "Not Mentioned is NOT Neutral." A regex/keyword collector cannot safely make that
judgment call — it would either under- or over-infer sentiment. So this collector only
does the mechanical half of normalization (who/when/what type/which tickers were
literally mentioned/how the post is linked to an original). The semantic half
(explicit_stance, evidence, thesis, catalyst, time_horizon, position_disclosure)
is left for a dedicated Claude analysis pass that reads each post's text and fills those
columns in — auditable, and never guessed by the pipeline itself.
post_id seen twice, e.g. a re-run): prevented at the DB
level — post_id is the primary key, inserts use ON CONFLICT DO NOTHING.referenced_post_id, the id of the underlying tweet. This is the join key a later
consensus step uses to collapse "5 accounts reposted the same tweet" into one underlying
event rather than 5 independent signals."Consensus must ultimately count unique independent accounts, not raw post count." This
collector stores everything needed to compute that correctly later (username per post,
referenced_post_id for de-duplicating shared reposts, explicit_stance once analyzed),
but does not compute a consensus score itself — that belongs to the "Market Discovery &
Consensus" stage downstream, once explicit_stance is populated. Roughly:
-- Sketch only — meaningful once explicit_stance is populated by Phase 2.
SELECT mentioned_tickers, explicit_stance, COUNT(DISTINCT username) AS unique_accounts
FROM posts
WHERE explicit_stance IS NOT NULL
GROUP BY mentioned_tickers, explicit_stance;
analyzed_at IS NULL, and for each one
(using only that post's own text/evidence) fills explicit_stance, evidence, thesis,
catalyst, time_horizon, and position_disclosure (only if explicitly stated), then
sets analyzed_at.referenced_post_id), and surfaces disagreement/consensus per ticker.config.ts hardcodes 10 accounts and ticker
extraction is cashtag-only; broader discovery (more accounts, sourcing tickers beyond
explicit cashtags, ranking/scoring) is future work once this pipeline is validated./run-collector works end-to-end with a real token, say the word and an interval
file can be added on a cron (nothing else about this val needs to change).GET /health → confirms token status + current row counts
GET /run-collector → pulls new posts right now (needs the token)
GET /posts?limit=20 → see what got normalized and stored
GET /runs → see run history / per-account errors