Skip to main content

⚓ AnchorX

The grounding layer for AI agents — verifiable, dated, cited knowledge instead of compressed memory.

PyPI Python Tests License MCP

One command in Claude Code: claude mcp add anchor -- uvx anchorx


1. The Problem

Most "AI + data" tools are fetchers. Give them a URL, they hand back a string, and then they forget everything. That string has:

  • no date — you can't tell if it's current or three years stale
  • no provenance — you can't open the exact spot it came from
  • no memory — the next question starts from zero
  • no dedup or conflict detection — ten copies of the same claim look like ten facts
  • no sense of absence — the model can't tell you what it doesn't have, so it fills the gap by hallucinating
flowchart LR
    U["URL / PDF / video"] --> F["🪣 Fetcher"]
    F --> S["raw string<br/>no date · no source · no memory"]
    S --> L["🤖 LLM"]
    L --> A["answer you cannot verify"]
    style F fill:#3a1a1a,stroke:#a33,color:#fff
    style A fill:#3a1a1a,stroke:#a33,color:#fff

When the answer is wrong, you have no way to check why.


2. The Solution

AnchorX is the layer above the fetchers. Every piece of knowledge it acquires is normalized into a single schema and stored in a local, embedded corpus — carrying a resolvable anchor back to a human-openable source, a publication date, a quality score, and freshness metadata.

flowchart LR
    U["URL / PDF / video / image"] --> A["⚓ AnchorX"]
    A --> D[("Document<br/>+ anchors<br/>+ provenance<br/>+ quality")]
    D --> C[("Local corpus<br/>SQLite + LanceDB")]
    C --> L["🤖 LLM (Claude)"]
    L --> R["answer + [20:47] · p.14 · ¶14<br/>citations you can click open"]
    style A fill:#0d2818,stroke:#2ea44f,color:#fff
    style R fill:#0d2818,stroke:#2ea44f,color:#fff

Success criterion: not "the AI stops being wrong." It's "when the AI is wrong, one click shows you exactly why."

Architecture at a glance — read it bottom-up

The durable value lives at L0/L1; the adapters at L2 are replaceable parts.

┌─────────────────────────────────────────────────────────────────────┐
│  L4  INTERFACE      MCP server  ·  CLI  ·  Python SDK                │
│      ask · search · ingest · research · coverage · fetch · clear     │
└────────────────────────────────┬────────────────────────────────────┘
┌────────────────────────────────▼────────────────────────────────────┐
│  L3  ORCHESTRATION  Budget Governor · Job Queue · Research · Sync    │
└────────────────────────────────┬────────────────────────────────────┘
        ┌────────────────────────┴───────────────────────┐
┌───────▼──────────────────────────┐   ┌─────────────────▼────────────┐
│  L2  ACQUISITION (adapters)       │   │  L2' RETRIEVAL               │
│  youtube · web_deep · web_search  │   │  pre-filter → BM25 + dense   │
│  documents · images               │   │  → RRF → cross-encoder rerank│
└───────────────┬───────────────────┘   └─────────────────┬────────────┘
┌───────────────▼─────────────────────────────────────────▼────────────┐
│  L1  NORMALIZATION   RawPayload → Document → Chunks → Vectors         │
│      provenance stamping · anchor assignment · quality scoring        │
└────────────────────────────────┬────────────────────────────────────┘
┌────────────────────────────────▼────────────────────────────────────┐
│  L0  CORPUS   SQLite (metadata · FTS5 · graph) · LanceDB (vectors)   │
│      blob cache · dedup · freshness · coverage · grounding           │
└─────────────────────────────────────────────────────────────────────┘

3. Usage

Install in Claude Code (30 seconds)

claude mcp add anchor -- uvx anchorx

No clone, no virtualenv, no Ollama. On first launch AnchorX auto-installs its browser, downloads a tiny CPU embedding model, creates its data directory, and serves 8 tools to Claude:

anchor_ingest · anchor_ask · anchor_search · anchor_research · anchor_coverage · anchor_fetch · anchor_refresh · anchor_clear

Then just talk to Claude:

"Ingest this PDF and these two articles into Anchor, then answer my question with citations." "What's my coverage on 'RAG'? Any gaps?" "Using only that video, what are the first words spoken?"

Or use the CLI

pip install anchorx

anchor ingest https://youtu.be/VIDEO_ID  "D:/reports/invoice.pdf"  https://example.com/post
anchor ask "what is the total on the invoice"        # grounded, cited, offline
anchor ask "how does chunking work" --url youtu.be   # scope to one source
anchor research "latest techniques for scraping SPAs"# discover → acquire → answer
anchor coverage "retrieval augmented generation"     # what do I have, what's missing
anchor clear --all                                   # wipe the corpus

Or the Python SDK

from anchor_ai.adapters.web_deep import acquire, render_page
from anchor_ai.corpus import DocumentStore, coverage

doc = acquire("https://example.com/post", render=render_page)   # L0→L2 ladder
print(doc.title, doc.quality_score, [s.heading for s in doc.sections])

report = coverage(store.conn, "chunking")
print(report.gaps)   # ["No source newer than 2025-12 …", "62% from one domain …"]

4. Why AnchorX

Four principles drive every decision in the design — and each one is a capability the fetcher-based ecosystem doesn't have.

Principle What it means How AnchorX delivers it
Provenance is the product An answer is only useful if you can open the source and check it Every chunk carries a resolvable anchor; citations deep-link to [20:47], p.14, ¶14
Pre-filter, not post-filter "Only videos over 50k views after March 2026" must be enforced inside retrieval Metadata filters push down into both BM25 and the vector scan before ranking
Absence is information The model must know the scope of what it has coverage() is a first-class tool that reports gaps, not a debug endpoint
Adapters are commodity Fetching is a solved, high-maintenance problem Thin adapters wrap proven libraries; swap one in an afternoon, nothing else changes

AnchorX vs. a plain fetcher / vanilla RAG:

Fetcher / vanilla RAG ⚓ AnchorX
Clickable, resolvable citations
Publication dates + freshness / staleness
Metadata pre-filter inside retrieval
Coverage report + gap detection
Near-duplicate + corroboration graph
Cross-session persistent memory
Flags what it couldn't ground

5. Technical Details

5.1 Ingestion — from a URL to a grounded, chunked Document

flowchart LR
    U["URL / file"] --> R{Router}
    R -->|youtube| Y[YouTube adapter]
    R -->|.pdf/.docx/.csv| DOC[Documents adapter]
    R -->|image| IMG[Images adapter]
    R -->|web| W[web_deep ladder]
    Y & DOC & IMG & W --> N["Normalize → Document<br/>provenance + anchors"]
    N --> C["Structure-aware<br/>chunking"]
    C --> E["Embed (model2vec, CPU)"]
    E --> S[("SQLite + LanceDB<br/>+ blob cache")]

5.2 Query — filter first, then fuse, then rerank, then ground

flowchart LR
    Q["Question"] --> F["Metadata<br/>pre-filter"]
    F --> B["BM25 (FTS5)"]
    F --> D["Dense (LanceDB)"]
    B --> X["RRF fusion<br/>k = 60"]
    D --> X
    X --> RR["Cross-encoder<br/>rerank → top 8"]
    RR --> A["Answer + citations<br/>+ grounding report"]

5.3 research() — the multi-round acquisition loop (capped at depth 3)

flowchart TD
    A[Question] --> B[Discover candidates]
    B --> C["Score: authority × recency × relevance / cost"]
    C --> D{Within budget?}
    D -->|no| G[Trim to top-N] --> E
    D -->|yes| E[Acquire → normalize → persist]
    E --> H[Retrieve + rerank]
    H --> I[Coverage check]
    I -->|gaps found, depth left| J[Refine queries] --> B
    I -->|sufficient| K[Answer + citations + grounding]

5.4 Adapters & their acquisition ladders

Adapters escalate through tiers only when needed — a static GET costs ~200 ms; a browser costs seconds.

🌐 web_deep — static → embedded → headless → stealth

flowchart LR
    L0["L0 · httpx + trafilatura<br/>~200ms"] --> J{needs JS?<br/>or near-empty?}
    J -->|no| OUT["Document"]
    J -->|yes| L15["L1.5 · __NEXT_DATA__ / JSON-LD<br/>zero browser cost"]
    L15 -->|found| OUT
    L15 -->|no| L2["L2 · Playwright Chromium<br/>network-idle + auto-scroll"]
    L2 --> L3["L3 · stealth · proxy · cookies<br/>(opt-in)"]
    L3 --> OUT

▶️ youtube — captions → ASR → diarization

Tier Method Quality
T0 Manual captions (yt-dlp) 0.95
T1 Auto captions 0.70
T2 faster-whisper + VAD 0.90
T3 + pyannote speaker diarization 0.92

Transcripts are cut into paragraphs on pauses, mapped onto chapters as sections, and every chunk is anchored to a timestamp — [12:04] resolves to youtu.be/ID?t=724.

📄 documents — PDF ladder + office + data cards

flowchart LR
    P0["P0 · text-layer check<br/>chars/page"] --> Q{scanned?}
    Q -->|no| P1["P1 · pypdfium2 text<br/>page-anchored"]
    Q -->|yes| P3["P3 · OCR (optional)"]
    P1 & P3 --> DOCX["DOCX / PPTX / EPUB → Markdown"]
    P1 --> TAB["CSV / XLSX → Data Card<br/>(schema · ranges · head)"]

Spreadsheets are never chunked as prose — they become a queryable Data Card (columns, dtypes, null %, ranges, sample rows).

🖼️ images — classify → route → VLM/OCR

flowchart LR
    I["image"] --> PH["pHash dedup"]
    PH --> CL{classify}
    CL -->|chart| VT["VLM → data table"]
    CL -->|diagram| VS["VLM → structure"]
    CL -->|screenshot| OV["OCR + VLM"]
    CL -->|photo| CAP["VLM caption"]
    VT & VS & OV & CAP --> TXT["content_md<br/>(text-retrievable)"]

Embedded images inside PDFs become child documents linked to their parent page — a chart on p.14 gets its own OCR/VLM description, retrievable by text query.

5.5 Anchors — what makes citation real

The difference between "RAG" and verifiable RAG:

Source Anchor payload Renders as Opens to
youtube {"t": 1247} [20:47] youtu.be/ID?t=1247
web {"sel": "…", "para": 14} ¶14 URL + scroll-to
document {"page": 14, "bbox": […]} p.14 PDF page + highlight
image {"region": […]} region image + box overlay
search_result {"rank": 3} SERP #3 original URL

5.6 Coverage & grounding — the differentiator

coverage(topic, filters) — what the corpus holds and where it's thin:

{
  "n_documents": 47,
  "by_source": { "youtube": 12, "web": 28, "document": 5, "image": 2 },
  "date_histogram": { "2024": 31, "2025": 14, "2026": 2 },
  "domain_concentration": 0.62,
  "authority": { "primary": 4, "secondary": 31, "unknown": 12 },
  "gaps": [
    "No source newer than 2025-12 — topic likely evolved",
    "62% of coverage from a single domain — low independence",
    "No primary/official documentation present"
  ]
}

Every ask() returns a grounding report — including unsupported_spans, the sentences that could not be entailed by the retrieved evidence:

{
  "grounding": {
    "chunks_used": 6, "sources_used": 4, "independent_domains": 3,
    "oldest_evidence": "2024-08-11", "newest_evidence": "2026-03-11",
    "unsupported_spans": ["Penguins architected the platform overnight."],
    "warnings": ["2 of 4 sources are stale (>180d)"]
  }
}

5.7 Quality scoring

One quality_score ∈ [0,1] per document — components stored separately so scores can be re-weighted without re-ingesting.

quality = 0.30 × acquisition_fidelity      # tier: T0=.95 T1=.70 T2=.90 · L0=.90 · P1=.95
        + 0.25 × extraction_completeness    # text ratio · table survival · no truncation
        + 0.20 × authority                  # primary source · domain · named author
        + 0.15 × recency_fit                # half-life decay vs. topic volatility
        + 0.10 × structure                  # headings · resolved sections · valid anchors

5.8 Project structure

anchor_ai/
├── core/          document · protocols · chunking · quality        (L1 keystone)
├── adapters/      youtube · web_deep · web_search · documents · images
├── corpus/        store · vectors · embeddings · retrieval · rerank
│                  blobs · dedup · freshness · coverage · grounding
│                  entailment · image_index                          (L0 durable value)
├── orchestration/ budget · queue · research · sync                  (L3)
├── interfaces/    cli · mcp_server                                  (L4)
└── bootstrap.py   first-run setup (browser + model)

5.9 Tech stack

Concern Choice Why
Metadata store SQLite + FTS5 one file, BM25 included, transactional
Vectors LanceDB embedded, native metadata pre-filtering
Embeddings model2vec static CPU embeddings — good semantics, no server, no GPU
Static extract trafilatura best boilerplate removal per CPU cycle
Browser Playwright superior waiting primitives + context pooling
YouTube yt-dlp the only thing that keeps working
PDF pypdfium2 fast text layer, escalate only when needed
MCP fastmcp least ceremony

5.10 Optional model backends

Everything above runs with zero external services. Heavier capabilities are opt-in extras, each with a graceful fallback:

pip install "anchorx[full]"     # everything below
pip install "anchorx[asr]"      # faster-whisper       — YouTube T2 ASR
pip install "anchorx[diarization]"  # pyannote.audio   — YouTube T3 speakers
pip install "anchorx[ocr]"      # pytesseract          — scanned docs / screenshots
pip install "anchorx[clip]"     # open-clip            — text ↔ image search
pip install "anchorx[stealth]"  # playwright-stealth   — web_deep L3

6. License

Released under the MIT License — see LICENSE for the full text.

MIT License · Copyright (c) 2026 Syed Muhammad Awais Gillani

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction … THE SOFTWARE IS PROVIDED "AS IS",
WITHOUT WARRANTY OF ANY KIND. See LICENSE for the complete text.

📦 PyPI · 🐙 GitHub · 🐛 Issues

Built as a grounding layer for Claude Code and any MCP-compatible agent.

Release files for anchorx 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for anchorx 0.1.1
File Size Uploaded
anchorx-0.1.1.tar.gz 97.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for anchorx 0.1.1
File Interpreter ABI Platform
anchorx-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size:245.9 kB

Release files / anchorx-0.1.1.tar.gz

Download URL anchorx-0.1.1.tar.gz
Size 97.5 kB
Tags Source
SHA-256 checksum
How to use checksums
f66e5c8e90a29dd926c0a7a1944c6b0a65f55e748ac27388d277c9941da77f66
BLAKE2b-256 checksum
How to use checksums
0980340c264655f7cd912a5a40142f85cafb7feaf80e0e0c08e02f78f58bdffd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 20, 2026.

Transparency log

Release files / anchorx-0.1.1-py3-none-any.whl

Download URL anchorx-0.1.1-py3-none-any.whl
Size 148.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
26aec37ac61e2c756f1c98045f9b69656c7fa4a906cba4575a00259293133d8f
BLAKE2b-256 checksum
How to use checksums
e1aaab2deaca60db0e0c0d295da89834103a5d4ecc67b4df4b5288e1cb1fef8f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 20, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page