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.
| 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.
Status: v0.1.0. The specification on this page is stable —
the guarantees are the contract; API names may still sharpen before 1.0.
npm publication under @sourcedhq is imminent; until then the
hosted API at sourced.run is the fastest
way to use the engine.
License: not yet granted — all rights reserved while the licensing decision is made. The intended direction is an open core (spec, reference implementation, conformance suite) with hosted history services on top.
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