Nexus
A Jev-powered AI news feed.

Nexus ingests AI news from multiple sources, dedupes it, classifies each item with a single Jev (TypeSafe System One) call, and serves the result as a reverse-chronological feed with filters and a guarded, extractive chat search. No graph database, no generative LLM in the request path.
Sources (arxiv, hackernews, github, twitter, rss)
β
βΌ
dedup.ts (URL β title β arXiv-id β Jaccard + entity fingerprints)
β
βΌ
PostgreSQL raw_items
β
βΌ
Jev enrichment (TypeSafe System One, model jev-latest, 1 call/item)
relevant? (Noul) Β· vertical (Choice) Β· event_type (Choice) Β· significance (Score)
β
βΌ
PostgreSQL feed_items (reverse-chron, indexed)
β
βΌ
Fastify API β GET /api/feed Β· GET /api/feed/meta Β· POST /api/chat
β
βΌ
React feed client β ActivityFeed, FilterBar, HotCards, guarded ChatBox
Quick Start
docker compose up -d
pnpm install
pnpm build
pnpm dev:api
pnpm dev:client
pnpm --filter @nexus/agent start
The client opens at http://localhost:5173 and the API serves at http://localhost:3001.
Monorepo Layout
| Package | Path | Description |
|---|
@nexus/shared | packages/shared | TypeScript interfaces, enums, constants, validation β imported by all packages |
@nexus/agent | packages/agent | Source adapters + dedup + Jev enrichment |
@nexus/api | packages/api | Fastify REST server for the feed and chat search |
@nexus/client | packages/client | React feed UI (Vite) |
Adding a Source Adapter
The primary contribution path is adding new data sources. Every adapter feeds raw items into the dedup + Jev enrichment pipeline, which classifies and inserts them into the feed automatically.
The RawItem Interface
interface RawItem {
source: string;
source_url: string;
title: string;
content: string;
published_at: string;
raw_metadata: Record<string, unknown>;
}
Write Your Adapter
Extend BaseAdapter and implement fetchItems(). You get rate limiting, exponential retry with backoff, and URL-based deduplication for free.
import type { RawItem } from "./types.js";
import { BaseAdapter } from "./base-adapter.js";
export class MySourceAdapter extends BaseAdapter {
name = "my-source";
priority = "P1" as const;
constructor() {
super({
pollIntervalMs: 30 * 60 * 1000,
rateLimitMs: 2000,
});
}
protected async fetchItems(): Promise<RawItem[]> {
const response = await fetch("https://api.example.com/items");
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
const data = await response.json();
return this.dedupeByUrl(
data.map((item: any) => ({
source: this.name,
source_url: item.url,
title: item.title,
content: item.body,
published_at: new Date(item.date).toISOString(),
raw_metadata: { id: item.id },
}))
);
}
}
Existing Adapters
| Adapter | Source | Priority | Poll Interval | Auth |
|---|
ArxivAdapter | arXiv RSS (cs.AI, cs.CL, cs.LG) | P0 | 30 min | None |
HackerNewsAdapter | HN Algolia API | P0 | 15 min | None |
GitHubTrendingAdapter | GitHub Search API | P0 | 60 min | Optional GITHUB_TOKEN |
TwitterAdapter | X/Twitter API v2 | P1 | 15 min | X_BEARER_TOKEN required |
RssAdapter | Company blogs, TechCrunch AI, The Verge AI, Simon Willison | P1 | 2h (default) | None |
Register Your Adapter
- Add your adapter to the adapter list in
packages/agent/src/start.ts:
import { MySourceAdapter } from "./sources/my-source.js";
const adapters: SourceAdapter[] = [
new HackerNewsAdapter(),
new ArxivAdapter(),
new GitHubTrendingAdapter(),
new MySourceAdapter(),
];
-
Write a test in packages/agent/src/sources/__tests__/ following existing patterns.
-
Run tests: pnpm --filter @nexus/agent test
Jev Enrichment
Every deduped RawItem gets exactly one call to TypeSafe's System One API (POST https://api.typesafe.ai/v1/systemone, model jev-latest, Bearer TYPESAFE_API_KEY). Jev is a decision model β it answers typed questions (Noul, Choice, Score) against structured JSON, it does not generate text. One call answers all four questions for an item:
is_ai_relevant (Noul) β dropped if confidence < 0.6
vertical (Choice) β one of the 21 Vertical values, or none
event_type (Choice) β launch | funding | release | acquisition | paper | update | shutdown | other
significance (Score) β a 5-point rubric mapped to 0.2β1.0
The excerpt shown in the feed is the first 280 characters of the source content β never an LLM summary, so it can't hallucinate.
Chat Search
POST /api/chat is a guarded, extractive search box, not a chatbot. It classifies the query with one Jev call (on-topic check, vertical/event-type/timeframe extraction), then returns real rows from feed_items via Postgres trigram search. There is no generative model anywhere in the chat path β there's nothing for a prompt injection to talk to.
MCP Server
The feed is exposed to agents as a public, read-only Model Context Protocol server over Streamable HTTP:
https://nexus.carapace.bot/mcp
| Tool | What it does |
|---|
search_news | Full-text search (phrases, OR, -exclude), filterable by vertical / event_type / source / since |
get_latest_news | Newest items first, same filters, cursor-paginated via next_cursor |
get_feed_stats | Total count plus counts per vertical and event_type |
Add it to Claude Code with claude mcp add --transport http nexus https://nexus.carapace.bot/mcp. Stateless (no sessions), extractive only β same validation as GET /api/feed, no model calls. Registry manifest: server.json.
Data Model
feed_items
id (uuid) Β· title Β· url (unique) Β· source Β· published_at Β· excerpt Β· vertical Β· event_type Β· significance (0.0β1.0) Β· created_at
Verticals
21 spatial-cluster categories, defined in packages/shared/src/types.ts (e.g. foundation_models, agents, safety_alignment, consumer_products, ...).
Event Types
launch | funding | release | acquisition | paper | update | shutdown | other
Contributing
- Fork the repo and create a branch:
feat/my-feature, fix/some-bug, or chore/cleanup
- Write conventional commit messages:
feat: add devto adapter, fix: handle empty RSS feed
- Run checks before opening a PR:
pnpm test
pnpm lint
pnpm typecheck
- Keep PRs focused β one adapter or feature per PR.
Environment Variables
| Variable | Required | Default | Description |
|---|
TYPESAFE_API_KEY | Yes | β | TypeSafe API key for Jev (System One) enrichment and chat classification |
POSTGRES_HOST | No | localhost | PostgreSQL host |
POSTGRES_PORT | No | 5432 | PostgreSQL port |
POSTGRES_DB | No | nexus | PostgreSQL database name |
POSTGRES_USER | No | nexus | PostgreSQL user |
POSTGRES_PASSWORD | Yes | β | PostgreSQL password |
X_BEARER_TOKEN | No | β | X/Twitter API bearer token (enables Twitter adapter) |
GITHUB_TOKEN | No | β | GitHub PAT (raises rate limits for GitHub adapter) |
API_KEY | No | β | API key for write/admin endpoints |
API_PORT | No | 3001 | API server port |
API_HOST | No | 0.0.0.0 | API server bind address |
CLIENT_PORT | No | 5173 | Client dev server port |
Tech Stack
- Database β PostgreSQL 16 (
raw_items, feed_items, pg_trgm search)
- API β Fastify
- Client β Vite + React
- Search β Fuse.js (client-side, loaded items), pg_trgm (server, chat + feed search)
- Agent β TypeSafe Jev (System One) for enrichment and chat classification
- Deploy β Railway (Postgres + API) + Vercel (client)
Cost
Enrichment is roughly 1 Jev call per new deduped item, at ~50β100 items/day across all sources, plus 1 Jev call per chat query. There is no Neo4j, no Redis, and no generative LLM anywhere in the pipeline β that's the entire compute bill.
License
MIT