One engine.
Every repo and site.
The Tri-Lens scoring engine is the single source of truth for every trust score at SiiYa. This page is the contract: how any repo, site, or service connects to it — securely, consistently, and the same way every time.
tri-lens-pipeline engine — treat response shapes as the working
contract, not a frozen spec, until this banner is removed.
1 · The model
Nothing computes its own trust scores. The engine (trilens-api,
deployed on Cloud Run) owns scoring; every surface is a consumer
that reads from it over HTTPS. This is what makes scoring seamless across repos:
add a new site tomorrow and it speaks to the same engine, the same way.
| Piece | What it is | Where it lives |
|---|---|---|
| Scoring engine | Scores articles, serves stats & feed | tri-lens-pipeline → Cloud Run |
| Consumer (site) | Reads scores, renders them | siiya-website, agent-command-center, future surfaces |
| This contract | How consumers connect | Mirrored here + in tri-lens-pipeline |
Engine base URL: https://trilens-api-782954600608.us-central1.run.app
— referenced once per consumer (see §5), never hard-coded in many places.
2 · Authentication — one API key per consumer
Every consumer (each site / service / repo) gets its own API key.
Keys are sent on every request in the X-Api-Key header. One key per
consumer means access can be rate-limited, rotated, or revoked for a single
surface without touching any other.
GET /feed HTTP/1.1
Host: trilens-api-782954600608.us-central1.run.app
X-Api-Key: tl_live_<your-consumer-key>
Accept: application/json
| Action | Owner | How |
|---|---|---|
| Issue a key | Platform team (tri-lens-pipeline) | One key per consumer, prefix tl_live_ (or tl_test_) |
| Store a key | Consumer | As a secret — env var / CI secret / hosting secret. Never in committed source. |
| Rotate / revoke | Platform team | Per-consumer, no impact on other surfaces |
3 · Security — secret keys never ship to the browser
| Consumer type | Key | Pattern |
|---|---|---|
| Server-side API, function, backend |
Secret key in env | Call the engine from the server; never expose the key to clients. |
| Browser / static site e.g. apps.siiya.online |
Domain-restricted publishable key | Key only works from allowlisted origins (Origin/Referer + CORS, §4). Or proxy through a tiny serverless function that holds the secret key. |
Recommendation for read-only public data (stats, feed) on browser surfaces: a domain-restricted publishable key + CORS allowlist gives the simplest customer experience while keeping the secret server keys server-side.
4 · CORS — origin allowlist
Browsers only let a page read the engine if the engine returns the page's origin in its CORS headers. So every new browser consumer must have its origin allowlisted on the engine. Without this, a correct key still yields a blocked request.
Access-Control-Allow-Origin: https://apps.siiya.online
Access-Control-Allow-Headers: X-Api-Key, Content-Type
Access-Control-Allow-Methods: GET, OPTIONS
Current/target allowlist: apps.siiya.online, admin.siiya.online, plus any partner origins added per consumer.
5 · Endpoints
/statsTop-line scoring totals (the numbers in the site’s stat bar).
{
"articles_scored": 31624,
"gold_articles": 412,
"avg_trust_score": 62.7,
"trust_dimensions": 3
}
/feedRecently scored articles (the “Today’s scored coverage” list). Array, or an object with items / articles / feed.
[
{
"title": "Central banks signal coordinated rate policy",
"source": "Reuters",
"url": "https://…",
"trust_score": 88
}
]
Score → badge tier
Every score maps to one of four tiers (consumers render the badge from the score):
| Tier | Range |
|---|---|
| Gold | 85 – 100 |
| Well-Sourced | 60 – 84 |
| Under Review | 30 – 59 |
| High Risk | 0 – 29 |
6 · The integration pattern (drop-in)
Every consumer follows the same shape: render a fallback first, then replace it
with live data. A surface must never break if the engine is
briefly unreachable — it degrades to last-known/sample data. This is exactly how
assets/scoring.js in this repo works; copy it as the reference.
const API = "https://trilens-api-782954600608.us-central1.run.app";
const KEY = window.TRILENS_KEY; // injected at deploy, never committed
async function loadFeed() {
renderFeed(SAMPLE, { sample: true }); // 1. never blank
try {
const res = await fetch(`${API}/feed`, {
headers: { "X-Api-Key": KEY, Accept: "application/json" },
});
if (!res.ok) return; // 2. keep fallback
const data = await res.json();
const items = Array.isArray(data) ? data : (data.items || data.articles || data.feed || []);
if (items.length) renderFeed(items, { sample: false }); // 3. go live
} catch (_) { /* engine unreachable — keep fallback */ }
}
7 · Adding a new repo or site
Four steps, every time:
- Request a key from the platform team — one
tl_live_key for the new consumer. - Get the origin allowlisted on the engine (CORS, §4) if it runs in a browser.
- Store the key as a secret (env / CI / hosting secret), injected at deploy — never committed (§3).
- Copy the consumer snippet (§6) — fallback first, live on success.
That’s the whole point of one engine + one contract: a new surface is a key, an allowlist entry, and ~20 lines — not a re-implementation of scoring.