SOURCED
Corroboration as a primitive

Sourced

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.

version 0.1.0 language TypeScript runtime deps 0 tests 22 · adversarial suite 14/14 packages @sourcedhq/*

1What Sourced is (and is not)

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.

Deliberately not

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.

2Quick start

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.

3Data model

Claim — what you put in

FieldTypeSemantics
id requiredstringStable identity of this report. Used to look up an entry in clusters; echoed nowhere else.
title requiredstringThe claim, in words. Event identity is derived from its meaning-bearing tokens (§4.1). The freshest wording seen for an event is retained.
origin requiredstringThe 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 requiredstring (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.

Verdict — what you get back

FieldTypeSemantics
corroborationnumber ≥ 1Count of distinct independent origins that have reported this event — this batch plus everything remembered in the store. Syndicated copies collapse (G3).
corroboratingSourcesstring[]The receipts: which other origins reported it. Never contains the claim’s own origin; capped at receiptsCap (default 6). Empty when corroboration = 1.
firstSeenAtstring (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" · nullDerived 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).

Injected collaborators

OptionShapeSemantics
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 optionalMap<claimId, origin[]> or plain objectPre-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[]) => voidWhere 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 optionalnumber (epoch ms)Injected clock. Makes runs deterministic and testable; defaults to Date.now().
config optionalPartial<Config>Threshold overrides — see §5. The defaults are the contract.

4The algorithm

Sourced maintains a persistent event store and folds each batch of claims into it. Five steps, all deterministic given the same inputs and clock:

4.1 · Identity

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.

4.2 · Matching (the dual gate)

If the key doesn’t match an existing event, a similarity fallback runs — but merging requires both gates to hold:

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.

4.3 · Counting

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).

4.4 · Signal

SignalCondition (defaults)Reading
confirmedcorroboration ≥ 4Broadly corroborated across independent origins.
breakingcorroboration ≥ 2 and published < 30 min agoCorroborated and fresh — by the upstream publish clock.
developingcorroboration ≥ 2Corroborated, not fresh.
nullcorroboration = 1A 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).

4.5 · Decay, bound, archive

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.

5Configuration

The defaults are the honesty contract. They encode undercount-never-overcount (G1). Every knob can be tightened freely; loosening mergeSimilarity or minSharedTokens trades away exactly the guarantee that makes a corroboration count worth trusting — do it knowingly or not at all.
KeyDefaultMeaningEffect of changing
mergeSimilarity0.60Jaccard floor for merging two events (gate 1 of 2).Higher = stricter identity, more undercounting. Lower = false merges → inflated counts.
minSharedTokens3Minimum 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.
confirmedAt4Origins needed for “confirmed”.Raise for high-stakes domains; lowering cheapens the strongest label.
corroboratedAt2Origins needed for “developing”/“breaking”.2 is the floor by definition — corroboration starts at a second source.
breakingWindowMs1 800 000 (30 min)Publish-age window for “breaking”.Match to your domain’s tempo: minutes for outages, hours for policy news.
eventTtlMs129 600 000 (36 h)Unseen this long → event retires (to the archive).Longer = corroboration accumulates across slower news cycles; matching window grows accordingly.
maxEvents400Working-set cap; most-recently-seen win.Size to your stream: roughly “events alive in one TTL window”.
receiptsCap6Max receipts returned per verdict.Display concern only — the count itself is never capped.
keyTokens8Tokens forming the deterministic event key.Rarely touched; affects exact-key hit rate, not safety (the dual gate still governs merges).
stopwordsEN + DE setTokens carrying no event identity.Replace when deploying into another language domain.

6Honesty guarantees G1–G7

Invariants, not preferences — each one is enforced by adversarial cases in the conformance suite, which you can run live right now.

#GuaranteeMechanism
G1Undercount, 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.
G2Never “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.
G3Independence 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.
G4Every 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.
G5Single sources stay bare. No badge, no signal, no receipts.Sourced only speaks when it has something corroborated to say. Silence protects credibility.
G6Signal 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.
G7Fail 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.

7Adversarial analysis

How would someone game a corroboration count — and why Sourced resists:

Astroturf many outlets

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.

Keyword-stuff a headline to hijack an event

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.)

Rush a “breaking” label

The urgency clock is the upstream publish time (G6). A planted first-sighting changes nothing; an old story newly corroborated reads “developing”, never “breaking”.

8Beyond news

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.

DomainA “claim” is…An “origin” is…The verdict answers
Newsa headlinean outletHow broadly is this reported?
Outage / incidenta failure reporta monitor / regionReal outage, or one flaky probe?
OSINT / verificationa field reportan account / feedHow many independent eyes confirm it?
Moderationa “this is bad” flaga distinct reporterGenuinely mass-flagged, honestly counted?
Sensor fusiona detectiona sensorDo multiple sensors agree an event occurred?
Market / event feedsan event printa data vendorIs 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.

9Hosted API

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"}]}'

10Conformance

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.

11Transparency log

@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.

12Packages, status, license

PackageContentsRuntime deps
@sourcedhq/coreThe primitive: assess(), types, createMemoryStore().none
@sourcedhq/conformanceG1–G7 as adversarial executable cases + runner for any implementation.core
@sourcedhq/logHash-chained transparency log: append, verify, inclusion proofs, JSONL storage, anchor CLI.none (node:crypto)
@sourcedhq/mcpMCP 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.

Built on 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