Back to Sourced
For builders

Evidence-linked context for conversational apps.

Register an app, pick or design a frame, then chat through Sourced or call native APIs. Your model still writes the reply. Sourced cheaply observes what was said—non-generative by default—so people can review candidates before anything is shared.

How it works

Your LLM still writes the reply

Sourced cheaply notices structure in the user's words against a frame you chose— non-generative by default. People review candidates before anything is shared.

ConcernLLM?
Chat / completions replyYes — your upstream model (BYOK)
Observation (default)No generative LLM (non_generative)
Assisted observationGenerative LLM only when a gate fires
Person confirmationHuman — never a model

Full endpoint returns table, observation modes, and OpenAPI links: Full builder guide (docs/product/API_BUILDER_GUIDE.md).

OpenAI drop-in

Chat through Sourced; observe in parallel

Point the OpenAI SDK (or curl) at Sourced's POST /v1/chat/completions. The reply is from your upstream model, unchanged. Sourced runs cheap non-generative observation against the app's default_frame_id in the background— never blocking or rewriting the chat response.

Auth split (both required)

  • X-Sourced-App: <app_id>
  • Authorization: Bearer <sourced_app_secret> — Sourced app auth (never forwarded upstream)
  • X-Upstream-Authorization: Bearer <openai_key> — forwarded as upstream Authorization

Response header x-sourced-trace-id, then GET /v1/observations/{trace_id} with Sourced app auth only. Register a starter frame first so default_frame_id is set.

OpenAI SDK note: api_key alone is not enough. Use the Sourced secret as api_key (so the SDK sets Authorization) and put the OpenAI key in default_headers["X-Upstream-Authorization"] with X-Sourced-App.

curl — completions

curl -s -D - -X POST "https://sourced.wayway.ai/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "X-Sourced-App: <your-app-id>" \
  -H "Authorization: Bearer <your-app-secret>" \
  -H "X-Upstream-Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "I care about building things that help people learn."}]
  }'
# Response body = upstream chat reply (unchanged).
# Response header x-sourced-trace-id = observation job id.

Python — OpenAI SDK

import os
from openai import OpenAI

# Auth split (required):
#   X-Sourced-App + Authorization:Bearer <sourced_app_secret>  → Sourced
#   X-Upstream-Authorization:Bearer <openai_key>              → forwarded upstream
# api_key alone is NOT enough — OpenAI SDK needs default_headers for Sourced + upstream.
client = OpenAI(
    base_url="https://sourced.wayway.ai/v1",
    # SDK sets Authorization from api_key → use the Sourced app secret here.
    api_key="<your-app-secret>",
    default_headers={
        "X-Sourced-App": "<your-app-id>",
        "X-Upstream-Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}",
    },
)

# Prefer with_raw_response so you can read x-sourced-trace-id
raw = client.chat.completions.with_raw_response.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "I care about building things that help people learn."}],
)
print(raw.parse().choices[0].message.content)  # reply from your upstream model
trace_id = raw.headers.get("x-sourced-trace-id")
# Then: GET /v1/observations/{trace_id} with Sourced app auth only (see curl below).

Use with_raw_response (or curl -D -) to read x-sourced-trace-id. Then poll observations:

curl — get observation

curl -s "https://sourced.wayway.ai/v1/observations/$TRACE_ID" \
  -H "X-Sourced-App: <your-app-id>" \
  -H "Authorization: Bearer <your-app-secret>"

Status is pending observed (or skipped / error). Candidates are non-generative against default_frame_id. Consent injection into the upstream prompt is Phase C — documented only, not built yet.

Try it

Quickstart

After you register an app and pick a frame, observe a turn with the native API. You get typed, evidence-linked candidates—non-generative by default—then a report of what landed against the schema.

1. Observe text

curl -X POST https://sourced.wayway.ai/v1/frames/<your-frame-id>/observe \
  -H "Content-Type: application/json" \
  -H "X-Sourced-App: <your-app-id>" \
  -H "Authorization: Bearer <your-app-secret>" \
  -d '{
    "person_id": "demo-user",
    "text": "I care about building things that help people learn.",
    "source_id": "turn_1"
  }'

What you get back

{
  "status": "ok",
  "trace_ids": ["tr_8c1f3a"],
  "trace_count": 1,
  "superseded_count": 0,
  "candidates": [
    {
      "trace_id": "tr_8c1f3a",
      "dimension": "tom.person.values.helping",
      "statement": "Cares about building things that help people learn.",
      "confidence": 0.87,
      "relation": "cares_about",
      "evidence": "I care about building things that help people learn."
    }
  ],
  "triggered_abilities": []
}

Each candidate trace has a dimension (the schema slot that matched), a relation (how the speaker relates, e.g. cares_about or stuck_on), a confidence score, and the exact evidence that triggered it. Candidates stay provisional until the person confirms or corrects them in review.

2. Report — dimensions scored

curl "https://sourced.wayway.ai/v1/frames/<your-frame-id>/report/demo-user" \
  -H "X-Sourced-App: <your-app-id>" \
  -H "Authorization: Bearer <your-app-secret>"

What you get back

{
  "status": "ok",
  "person_id": "demo-user",
  "dimensions": [
    {
      "dimension": "tom.person.values.helping",
      "status": "matched",
      "confidence": 0.87,
      "trace_count": 2,
      "relation": "cares_about",
      "evidence_quote": "I care about building things that help people learn."
    },
    {
      "dimension": "tom.person.direction.practices",
      "status": "gap",
      "confidence": 0.0,
      "trace_count": 0,
      "description": "Active commitments and daily practices"
    }
  ],
  "surprises": [
    {
      "dimension": "tom.person.energy.flow",
      "relation": "alive_in",
      "confidence": 0.72,
      "evidence_quote": "Time disappears when I'm making things."
    }
  ],
  "posture_distribution": {
    "receiving": 2,
    "moving": 0,
    "participating": 1
  },
  "coverage": 0.33,
  "total_traces": 3
}

Every dimension comes back scored: matched dimensions are schema slots with traces, gaps are dimensions the person hasn't addressed yet. Surprises are traces that landed outside your schema — things you didn't ask for but the person expressed. The posture_distribution shows the balance of receiving, moving, and participating across the kept traces.

3. Fetch insights

curl "https://sourced.wayway.ai/v1/frames/<your-frame-id>/insights?person_ids=demo-user" \
  -H "X-Sourced-App: <your-app-id>" \
  -H "Authorization: Bearer <your-app-secret>"

Insights response

{
  "spans": [...],
  "traces": [
    {
      "dimension": "tom.person.values.helping",
      "about": "helping people learn",
      "relation": "cares_about",
      "confidence": 0.87,
      "statement": "Cares about building things that help people learn.",
      "status": "confirmed"
    }
  ],
  "threads": [...],
  "connections": []
}

What just happened

Observe matched your text against the frame's schema and extracted typed claims — each trace has a dimension, a verb-style relation, confidence, and evidence. Report shows every dimension scored — what landed (matched), what's missing (gaps), and what surprised you (traces outside your schema). Insights aggregates all traces into spans, threads, and connections.

Full example

Full Python Example

A complete script that observes text and fetches insights. Copy it, fill in your credentials, and run.

sourced_quickstart.py

#!/usr/bin/env python3
"""Sourced quickstart — observe, report, fetch insights."""
import requests

BASE = "https://sourced.wayway.ai"
APP_ID = "<your-app-id>"
APP_SECRET = "<your-app-secret>"
FRAME_ID = "<your-frame-id>"

headers = {
    "X-Sourced-App": APP_ID,
    "Authorization": f"Bearer {APP_SECRET}",
    "Content-Type": "application/json",
}

# 1. Observe some text (frame-scoped: returns review-ready candidates)
resp = requests.post(f"{BASE}/v1/frames/{FRAME_ID}/observe", headers=headers, json={
    "person_id": "demo-user",
    "text": "I feel energized when I build products that help people.",
    "source_id": "turn_1",
})
print("Observe:", resp.status_code, resp.json())

# 2. Report — every dimension scored against the author's frame
resp = requests.get(
    f"{BASE}/v1/frames/{FRAME_ID}/report/demo-user",
    headers=headers,
)
result = resp.json()
matched = [d for d in result["dimensions"] if d["status"] == "matched"]
gaps = [d for d in result["dimensions"] if d["status"] == "gap"]
print(f"Matched: {len(matched)}, Gaps: {len(gaps)}, Surprises: {len(result['surprises'])}")
print(f"Coverage: {result['coverage']:.0%}")

# 3. Fetch insights (spans, threads, connections)
resp = requests.get(
    f"{BASE}/v1/frames/{FRAME_ID}/insights",
    headers=headers,
    params={"person_ids": "demo-user"},
)
print("Insights:", resp.status_code, resp.json())
Try it live

Interactive API Documentation

Every endpoint is documented with request/response schemas and a live “Try it out” button. No setup required — test directly from your browser.

Key Endpoints

OperationEndpointWhat it does
ObservePOST /v1/observeExtract claims from text — returns kept trace ids and triggered abilities
Frame ObservePOST /v1/frames/:id/observeObserve within a frame context — uses the frame's schema and dimensions; returns review-ready candidates
Completions drop-inPOST /v1/chat/completionsOpenAI-shaped passthrough — upstream reply + x-sourced-trace-id; non-generative observation against default_frame_id
ObservationsGET /v1/observations/:trace_idDrop-in observation job status and candidates (Sourced app auth)
ReportGET /v1/frames/:id/report/:personAll dimensions scored (matched + gaps), surprises, posture distribution, coverage
ProfileGET /v1/frames/:id/profile/:personFull person model — traces, patterns, coverage across all sessions
Session SummaryGET /v1/frames/:id/session-summary/:personPer-session breakdown — what landed in this conversation
ChatPOST /v1/chat/:frame/:personSSE streaming conversation — sends turns, streams facilitated responses
SynthesizePOST /v1/frames/:id/synthesize/:personGenerate narrative — portrait, patterns, or summary from traces
InsightsGET /v1/frames/:id/insightsAggregated view — spans, threads, connections across subjects
Review QueueGET /v1/people/:person/reviewWhat the system currently believes, each claim with its evidence quotes
ReviewPOST /v1/traces/:id/reviewConfirm, correct, reject, hide, contest, or nuance a claim — corrections supersede, evidence is never erased
GrantsPOST /v1/people/:person/grantsRecord a person's consent for this app to read their context — revocable, scoped by frame/dimension patterns
ReceiptsGET /v1/traces/:id/receiptsWho accessed this claim, when, for what purpose — written on every context read
VIA previewPOST /v1/stance/via/assessEvidence-linked VIA strength hypotheses from text — preview only; nothing is persisted

Advanced / deferred

Match and cohort-style compose remain available for Studio and research workflows. They are not the primary builder wedge.

OperationEndpointWhat it does
MatchGET /v1/frames/:id/matchesCross-subject connections — shared dreams, complements, tensions
Stance layers

Deterministic reading + VIA preview

The local stance library (sourced/stance/) detects surface stance and proposes typed claim relations without silently editing the person model. Person confirmations are governance actions. See Method → stance for the four-layer model.

HTTP (preview only)

POST /v1/stance/via/assess returns evidence-linked VIA hypotheses. Journal text is not written to Sourced memory. Studio UI: /studio/via.

Local demos & shadow

python examples/via_profile/assess_corpus.py ./journal --db via.db
python examples/via_profile/chat.py via.db
python scripts/eval_stance_micro.py
SOURCED_STANCE_SHADOW=on  # metrics → runs/stance_shadow/
python scripts/stance_shadow_report.py
When you're ready to build

Register & choose a frame

Sign in to create API credentials and start integrating.

Sign In
Reference
Architecture

Frames define what your AI listens for

Grant Application

Replaces grant forms with conversation. Looks for the problem, the impact, and your specific role — listens for evidence of accountability.

Values in Action

A demo frame for evidence-linked character-strength hypotheses. It exercises the same reviewable detection contract as other frames; it is not a psychometric assessment or a replacement for the VIA survey.

Coaching Onboarding

Replaces intake forms with guided dialogue. Looks for grounding, vision, obstacles, and commitment — asks where you are and what you're ready to change.

Aristotle's Golden Mean Demo

A playful philosophical demo. Looks for virtues, their excess, and their deficiency — find the mean between too much and too little.

Developer model

Keep objects simple, keep behavior declarative

Core primitives

Span → Trace → Thread. Data stays stable; behavior is configured in YAML.

Declarative YAML

Schema, Attunement, and Abilities define ontology, retrieval, and outcomes — without hardcoding app semantics.

Capability-driven outcomes

When confirmed traces meet an ability's requirements, Sourced resolves parameters and hands them to your app. You define what happens next.

Advanced / deferred

Compose Targets (match & cohort)

Targets with type: "compose" trigger synthesis instead of extraction. When enough evidence accumulates, the Fractal Weave engine can compose an artifact — a portrait, match summary, or cohort theme — from the participant's own words. Self (N=1) portrait compose remains useful in Studio; dyad match and cohort theme are deferred from the primary builder journey.

Relation vocabulary

Four questions + one meta-dimension. MICE, not MECE.

Every trace gets a relation — how the speaker relates to the thing the trace is about. These ten verbs are the posture vocabulary, the personal-ops default (frames can declare their own). Verb predicates mean database queries read like sentences.

Held
settled
Seeking
in motion
Values
axiology
cares_about
Clear values, deep commitments
“I care deeply about honesty”
torn_between
Two goods colliding
“I want freedom but need stability”
Knowledge
epistemology
knows
Stable skills, firm beliefs
“I’m good at systems thinking”
wondering
Testing ideas, uncertain
“Maybe I’d thrive in a smaller team”
Direction
teleology
working_on
Active practices, doing
“I started writing every morning”
reaching_for
Aspirations, not yet started
“I want to build something meaningful”
Energy
phenomenology
alive_in
Flow, vitality, joy
“Time disappears when I’m making things”
stuck_on
Blocked, depleted, stuck
“I can’t take that risk right now”
Sensemaking
hermeneutics
META
means
Settled stories, made peace
“That failure taught me resilience”
remaking
Story is shifting
“I’m starting to see it differently”

Three ToM Layers

tom.author
The Facilitator’s Philosophy

Rules and intent from worldview config, loaded before conversation starts and applied as prompt policy plus evidence strictness.

tom.person
The State Vector

System’s evolving belief about participant, multiple signals per turn (MICE).

tom.system
System Confidence

How honest the system is about its own uncertainty.

Platform Gate

ConfidentUncertain
HeldReflectWait
SeekingOfferBe honest
ConstraintUnblockBe honest

Tension is relational (two held cares colliding), not a row.

MICE, not MECE: Human experience overlaps. One sentence fires multiple relations — reaching_for + stuck_on + torn_between simultaneously. The vocabulary is Mutually Inclusive. MECE discipline applies to the processing pipeline (Constitution → Interpret → Update → Gate → Speak), not the perception layer.

How it observes

Observation is a dial, not a switch

Product observation modes are off, non_generative, assisted, and generative. Chat wire fields still accept legacy extraction_mode names; the API translates them once at ingress. The reference flow defaults to non_generative observation. Billing entitlement (core / assisted) constrains which modes an app may request — it is not the runtime switch itself. “No generative LLM in observation” does not mean “no LLM-written chat reply.”

off (legacy: none)

No observation or candidate extraction for this turn. Chat reply generation remains a separate policy.

No observation cost

non_generative (legacy: embeddings_only / keyword)

Rules + embeddings/markers only — no generative LLM for observation. Chat reply generation is separate and may still use an LLM.

Core / cheapest observation

assisted (legacy: smart)

Same as non_generative, plus a generative LLM for observation only when a declared gate fires. Every escalation should record a reason.

Assisted entitlement

generative (legacy: full)

Explicit generative observation. Useful for diagnostics; must not be a silent frame default. Product default target is non_generative.

Most expensive observation
Consent loop

Reliability lives in the loop

A candidate that the user confirms is just as reliable as a rich AI-synthesized candidate. The user's confirmation is what makes claims true.

1. Observe
System notices signal
2. Confirm
User says "Save"
3. Grow
The map updates

Standard Pages

  • /studio: design programs, send invites, review candidate traces.
  • Conversational Sessions (/chat): participant-facing guided response.

Common Friction

  • Returns 200 but no traces: run validate and inspect issues for source collisions.
  • Need frame-level rollups: use the /insights endpoint for a cross-subject view.
  • Preset is too heavy: start with tom_base and add specialized targets as needed.