Given a stream of claims from many origins, Sourced tells you how many independent sources corroborate each one, since when, and hands you the receipts. It says “confirmed by sources” — never “the truth.” This page is the specification.
Every feed, every aggregator, every “is this real?” moment re-solves the same problem badly or not at all: a thousand items, each equally loud, with no signal for which ones the world actually agrees happened. The missing layer is the oldest heuristic in journalism — get a second source — computed continuously over a live stream. Sourced ships that layer as one small, dependency-free function.
The framing is load-bearing: Sourced never claims a thing is true. Truth is not a property you can compute from headlines — corroboration is. “Six independent outlets report this, first seen 17:04” is a checkable fact about the world’s reporting, not a verdict handed down. This distinction is the difference between a trust layer that earns credibility and one that inflates it until it pops.
The surface stays this small on purpose. A building block does one thing well enough that a hundred products can lean on it — the narrower the surface, the wider the reach.
import { assess, createMemoryStore } from "@sourcedhq/core";
const store = createMemoryStore(); // or your KV / SQLite / file — anything with load()/save()
const verdicts = await assess(
[
{ id: "a", title: "Central bank raises rates", origin: "reuters", publishedAt: "2026-01-01T12:00:00Z" },
{ id: "b", title: "Central bank raises rates", origin: "bbc", publishedAt: "2026-01-01T12:04:00Z" },
{ id: "c", title: "Celebrity opens restaurant", origin: "gossip", publishedAt: "2026-01-01T12:05:00Z" },
],
{ store },
);
// verdicts[1] — the BBC claim:
// {
// corroboration: 2, // distinct independent origins reporting this event
// corroboratingSources: ["reuters"], // the receipts: who else (never the claim's own origin)
// firstSeenAt: "2026-01-01T12:…", // when THIS system first saw the event (persists across runs)
// signal: "breaking" // "confirmed" | "breaking" | "developing" | null
// }
//
// verdicts[2] — the single-origin claim:
// { corroboration: 1, corroboratingSources: [], firstSeenAt: "…", signal: null } // bare, by design (G5)
One call. Verdicts come back in input order, null for claims
that cannot be assessed (e.g. a title with no meaning-bearing tokens) —
those simply pass through unlabeled. assess never throws into
the stream it annotates (G7).
Prefer to try it without installing anything? The hosted playground and API live at sourced.run.
| Field | Type | Semantics |
|---|---|---|
| id required | string | Stable identity of this report. Used to look up an entry in clusters; echoed nowhere else. |
| title required | string | The claim, in words. Event identity is derived from its meaning-bearing tokens (§4.1). The freshest wording seen for an event is retained. |
| origin required | string | The source making the claim — the unit of independence (G3). Two claims share an origin iff these strings are equal, so normalize upstream (e.g. always "reuters", not sometimes "Reuters UK"). |
| publishedAt required | string (ISO 8601) | When the report was published, per the upstream feed. Drives the urgency signal (G6). An unparseable value disables “breaking” for that claim but nothing else. |
| Field | Type | Semantics |
|---|---|---|
| corroboration | number ≥ 1 | Count of distinct independent origins that have reported this event — this batch plus everything remembered in the store. Syndicated copies collapse (G3). |
| corroboratingSources | string[] | The receipts: which other origins reported it. Never contains the claim’s own origin; capped at receiptsCap (default 6). Empty when corroboration = 1. |
| firstSeenAt | string (ISO 8601) | When this system first saw the event. Persists across runs via the store — the accumulated first-seen timeline is data you cannot reconstruct later. |
| signal | "confirmed" · "breaking" · "developing" · null | Derived label (§4.4). null for single-origin claims — silence protects credibility (G5). Note what is absent: there is no “true”, no score of veracity (G2). |
| Option | Shape | Semantics |
|---|---|---|
| store optional | { load(), save(store) } | Event memory between runs. Both methods may be sync or async; any durable KV works (Redis, SQLite, a file, memory). Omitted → corroboration is computed within the batch only. Load/save failures degrade gracefully (G7). |
| clusters optional | Map<claimId, origin[]> or plain object | Pre-grouped “these origins reported the same event in this batch”, produced by any clustering you like — an LLM pass, regex, embeddings, an upstream editor. Sourced only counts; it never groups. No entry → the claim contributes just its own origin. |
| archive optional | (retired: StoredEvent[]) => void | Where events go when they leave the working set (TTL or cap, §4.5) — into your archive, not into oblivion. The full history of first-seen timelines is the long-term asset. |
| now optional | number (epoch ms) | Injected clock. Makes runs deterministic and testable; defaults to Date.now(). |
| config optional | Partial<Config> | Threshold overrides — see §5. The defaults are the contract. |
Sourced maintains a persistent event store and folds each batch of claims into it. Five steps, all deterministic given the same inputs and clock:
A title is reduced to meaning-bearing tokens: lowercased, punctuation stripped, stopwords removed (EN + DE built in, injectable), tokens shorter than 3 characters dropped. The deterministic event key is the sorted set of the top 8 tokens. Same key → same event, no similarity math needed.
If the key doesn’t match an existing event, a similarity fallback runs — but merging requires both gates to hold:
0.60, and3 shared meaning-bearing tokens.
Why both: short headlines can reach high Jaccard from two words shared by
chance ("Tesla recall" vs "Tesla recall expands"
scores 0.67 on two tokens). The token floor blocks that; the similarity
floor blocks keyword-stuffed titles that share 3 tokens buried in noise.
This is guarantee G1 in mechanical form: when in doubt, do not
merge — a missed corroboration costs little, a false “confirmed” costs the
only thing that matters.
A matched claim joins its event: the event’s origin set becomes the union
of every distinct origin ever seen for it (this batch ∪ history), and the
freshest title wording is retained.
corroboration = |distinct origins|. The receipts are the
other origins, capped for display. Articles are never counted — origins
are (G3).
| Signal | Condition (defaults) | Reading |
|---|---|---|
| confirmed | corroboration ≥ 4 | Broadly corroborated across independent origins. |
| breaking | corroboration ≥ 2 and published < 30 min ago | Corroborated and fresh — by the upstream publish clock. |
| developing | corroboration ≥ 2 | Corroborated, not fresh. |
| null | corroboration = 1 | A single origin stays unlabeled. Always (G5). |
The age test uses the claim’s published timestamp — a
reliable upstream value — never Sourced’s own firstSeenAt
guess. Consequence: an identity mistake can shift a count, but it can
never invent urgency (G6).
The working set stays bounded so matching stays sharp: events
unseen for 36 h retire, and the store caps at the 400 most-recently-seen
events. Retiring events are handed to your archive sink rather
than deleted — corroboration reflects a living window, while the full
history (every event, every first-seen timestamp) accumulates forever in
your archive. That history is the part of a Sourced deployment nobody can
copy out of a feed later.
| Key | Default | Meaning | Effect of changing |
|---|---|---|---|
| mergeSimilarity | 0.60 | Jaccard floor for merging two events (gate 1 of 2). | Higher = stricter identity, more undercounting. Lower = false merges → inflated counts. |
| minSharedTokens | 3 | Minimum shared meaning tokens for a merge (gate 2 of 2). | Higher = resists short-title coincidence even harder. Lower than 3 re-opens the two-word-overlap trap. |
| confirmedAt | 4 | Origins needed for “confirmed”. | Raise for high-stakes domains; lowering cheapens the strongest label. |
| corroboratedAt | 2 | Origins needed for “developing”/“breaking”. | 2 is the floor by definition — corroboration starts at a second source. |
| breakingWindowMs | 1 800 000 (30 min) | Publish-age window for “breaking”. | Match to your domain’s tempo: minutes for outages, hours for policy news. |
| eventTtlMs | 129 600 000 (36 h) | Unseen this long → event retires (to the archive). | Longer = corroboration accumulates across slower news cycles; matching window grows accordingly. |
| maxEvents | 400 | Working-set cap; most-recently-seen win. | Size to your stream: roughly “events alive in one TTL window”. |
| receiptsCap | 6 | Max receipts returned per verdict. | Display concern only — the count itself is never capped. |
| keyTokens | 8 | Tokens forming the deterministic event key. | Rarely touched; affects exact-key hit rate, not safety (the dual gate still governs merges). |
| stopwords | EN + DE set | Tokens carrying no event identity. | Replace when deploying into another language domain. |
Invariants, not preferences — each one is enforced by adversarial cases in the conformance suite, which you can run live right now.
| # | Guarantee | Mechanism |
|---|---|---|
| G1 | Undercount, never overcount. Sourced would rather miss a real corroboration than manufacture a false one. | Dual-gate matching (§4.2). A false “confirmed” is a reputation balloon that pops; all tuning errs toward saying less. |
| G2 | Never “true,” only “corroborated.” Confirmation is not truth, and Sourced does not blur the two. | The output vocabulary is N sources / confirmed / developing / breaking — statements about reporting, with receipts. No veracity field exists. |
| G3 | Independence is by origin, not by article. Ten copies of one wire story across ten pages count as one. | Origins are a set; duplicates collapse on entry, whether they arrive as separate claims or inside a cluster. |
| G4 | Every verdict carries its receipts. A count is never shown without who and since when. | corroboratingSources + firstSeenAt on every corroborated verdict — falsifiable by design; the reader can check the named sources. |
| G5 | Single sources stay bare. No badge, no signal, no receipts. | Sourced only speaks when it has something corroborated to say. Silence protects credibility. |
| G6 | Signal rides the reliable clock. A matching error can shift a count but can never invent urgency. | “Breaking” is computed from the upstream publish time, never from Sourced’s own first-seen estimate. |
| G7 | Fail open, never break. Degrading to “no verdict” is always safe; crashing is not. | Every step is best-effort: dead storage, empty clusters, malformed input → claims pass through unlabeled. assess never throws. |
How would someone game a corroboration count — and why Sourced resists:
Inflating a count requires genuinely distinct origins, not many articles — G3 collapses syndication and copies to one. The cost of faking corroboration is the cost of standing up many independent sources, which is the real-world cost of the thing being true enough to report.
Sharing 3 tokens with a real event is easy; passing Jaccard ≥ 0.60 at the same time requires the titles to genuinely be about the same thing. The stuffed title fails gate 1 and stays what it is: a single-origin claim with no signal. (This exact attack is a live test case — watch it fail.)
The urgency clock is the upstream publish time (G6). A planted first-sighting changes nothing; an old story newly corroborated reads “developing”, never “breaking”.
The unit Sourced operates on is (claim, origin, timestamp)
plus a pluggable way to group claims into events — not “a news article.”
Any domain with independent reports of discrete events fits: swap the
clustering and the storage, keep the counting and the guarantees untouched.
| Domain | A “claim” is… | An “origin” is… | The verdict answers |
|---|---|---|---|
| News | a headline | an outlet | How broadly is this reported? |
| Outage / incident | a failure report | a monitor / region | Real outage, or one flaky probe? |
| OSINT / verification | a field report | an account / feed | How many independent eyes confirm it? |
| Moderation | a “this is bad” flag | a distinct reporter | Genuinely mass-flagged, honestly counted? |
| Sensor fusion | a detection | a sensor | Do multiple sensors agree an event occurred? |
| Market / event feeds | an event print | a data vendor | Is this move corroborated across vendors? |
The outage row isn’t hypothetical — it ships as a runnable example in the
repository, tuned with confirmedAt: 3 and a 10-minute breaking
window, using the identical engine.
A public, stateless instance of the engine runs at
sourced.run — paste claims in the
playground or POST /api/assess directly. Full request/response
reference, limits, and examples in curl, JavaScript and Python live there.
curl -s https://sourced.run/api/assess \
-H "content-type: application/json" \
-d '{"claims":[{"id":"a","title":"…","origin":"reuters","publishedAt":"2026-07-10T21:00:00Z"}]}'
The guarantees ship as an executable, adversarial test suite —
@sourcedhq/conformance. It targets the function shape,
not this implementation: point it at any engine that claims to count
sources and it reports, case by case, whether that engine is
Sourced-honest or just counting volume.
import { runConformance } from "@sourcedhq/conformance";
const report = await runConformance(myAssessImplementation);
// { passed: 14, failed: 0, results: [{ id, guarantee, pass, detail }, …] }
The reference engine runs the suite on every commit in CI, and you can run it against the live deployment in your browser at sourced.network — it executes fresh every time, no cached results.
@sourcedhq/log chains verdict batches into a tamper-evident
hash chain: each record commits to the previous record’s hash
(Certificate-Transparency style, SHA-256 over canonicalized JSON). Publish
the chain head anywhere public — a git commit, OpenTimestamps — and the
entire history up to that point becomes verifiable: rewriting any
past verdict breaks every hash after it.
This turns “we have issued honest verdicts since day one” from a marketing line into a checkable claim. Record format, verification semantics and a hosted verifier live at sourced.network.
Reference for common HTTP status codes, API response errors, and troubleshooting steps across the Sourced network and API endpoints:
| HTTP Status | Error Code / Payload | Root Cause | Resolution Step |
|---|---|---|---|
401 Unauthorized |
"malformed key (expected sk_src_…)" |
Missing or invalid Authorization: Bearer header format. |
Ensure key starts with sk_src_. Alternatively pass X-Sourced-Key header. |
401 Unauthorized |
"unknown or revoked key" |
The provided API key is disabled, revoked, or non-existent in KV. | Check key status in the Control Deck or request a replacement key at hello@tickwire.news. |
401 Unauthorized |
"missing Clerk session token" |
Dashboard proxy attempt without an active Clerk JWT. | Sign in via Clerk on the Control Deck (sourced.run/dashboard/) to renew session. |
429 Too Many Requests |
"rate limit: N requests/minute" |
Per-minute budget exceeded (anon: 60/min, key: 600/min, append: 30/min). | Inspect Retry-After header (seconds). Acquire an API key to raise limits 10x. |
400 Bad Request |
"name required" / "invalid JSON" |
Malformed JSON body or missing required request parameters. | Verify payload structure against openapi spec; check required id, title, origin, publishedAt. |
503 Service Unavailable |
"CLERK_SECRET_KEY not configured" |
Environment variable missing on deployment. | Configure CLERK_SECRET_KEY and SOURCED_ADMIN_SECRET in Vercel environment settings. |
502 Bad Gateway |
"verification failed: ..." |
Upstream Clerk REST API or KV lookup network timeout. | Sourced fails open for public read streams (G7). For admin proxy commands, retry the request. |
| Package | Contents | Runtime deps |
|---|---|---|
| @sourcedhq/core | The primitive: assess(), types, createMemoryStore(). | none |
| @sourcedhq/conformance | G1–G7 as adversarial executable cases + runner for any implementation. | core |
| @sourcedhq/log | Hash-chained transparency log: append, verify, inclusion proofs, JSONL storage, anchor CLI. | none (node:crypto) |
| @sourcedhq/mcp | MCP server for AI agents — assess (session memory), verify_chain, run_conformance. Server name: sourced. | core, log, conformance |
Agent-readable summaries: /llms.txt on every Sourced domain.
# the engine
npm install @sourcedhq/core
# give it to your AI agent (Claude Code)
claude mcp add sourced -- npx -y @sourcedhq/mcp
Status: v1.0.0, published on npm — the stability promise
is in force: the guarantees, the verdict shape and the hosted
/api/v1/* paths do not break. No install needed to evaluate:
the hosted API at sourced.run runs the
same engine.
License: MIT for all code — use it, embed it, fork it. The Sourced name and mark are reserved: forks ship under their own name, and “Sourced-conformant” is a claim you earn by passing the conformance suite (see TRADEMARK-POLICY in the repository). Source: github.com/Ryan-hasi/sourced.
Tickwire is built on Sourced — and Sourced is proven by Tickwire. Every “✓ N sources” badge in the app is a Sourced verdict with receipts, computed live over the world’s reporting, all day, every day. The app gives the engine a production proving ground; the engine gives the app a trust layer nobody else ships. We are proud of both directions.
Contact: hello@tickwire.news