oll.am · Plan → Execution · Code-grounded build spec · 2026-07-07

oll.in — plan → execution

The companion to the solution architecture: the exact, code-grounded change-sets to build oll.in — real file paths and function names verified against the running services, the three new oll-write verbs, the one new service, the Core billing truth, and each POC written as a literal runnable spec. Verified against the live code, not guessed.
Grounded: file paths + patterns read from the real services Two corrections: oll-write is stateless · Core already does subscriptions POC-C result: scout source = Jooble (+ Arbeitnow)

POC results executed 2026-07-07 · all three branches green

The two master-gate lanes plus the RAG lane were built on feat branches with synthetic data, keyless, and verified by running — tests, a live tick, and (for oll-scout) Docker. All landed as draft PRs, nothing merged, nothing deployed.

Branch / PRWhat ranResult
poc/ollin-dossier-verbs
PR #87
3 verbs added to oll-write; pytest; ruff; offline poc_dossier.py against synthetic CV+JD.172 passed (+22), ruff clean, OpenAPI valid. All 3 verbs 200 offline. The CV emitted a literal gap marker [add: German level not stated — confirm C1] instead of inventing it — the groundedness rule works.
poc/oll-scout
PR #88
New service, fixture provider (7 synthetic CH/DACH roles); pytest; --once tick; live gunicorn + Docker.32 passed, ruff clean; Docker image healthy. Tick pulled 7 → scored 7 → 4 strong (≥60) → digest "sent": top match Senior Backend Engineer, Helvetia Fintech · Zürich [91]. Idempotency + SDK-boundary proven.
poc/ollin-rag-synthetic
PR #86
Synthetic candidate corpus → seed → retrieve → assemble cited context; abstain test.5 passed. 5 docs / 26 chunks ingested; job facets retrieved real passages (cos 0.10–0.36) → 15 numbered [n] evidence items; off-corpus query abstained (0.072 < floor, no fabricated citation). Sandboxed to a local stand-in using oll-memory's own default embedder; prod memory never touched.
What the POCs proved together: the whole dossier loop runs end-to-end on synthetic data — corpus → cited retrieval (honest abstain) → grounded verb output (honest gap-marker) — and the Career Radar backend ticks fixture→score→digest with the exact house patterns. Both master gates are mechanically green. The two remaining gates need Sam: (1) judge dossier send-worthiness against a real model (the POCs use the mock/offline model, so they prove the pipeline, not the prose quality); (2) the free Jooble API keySCOUT_PROVIDER=jooble to confirm live CH coverage (the POC-C concentration-risk check).
Honest scope: these verify feasibility and shape, not production readiness or real-model quality — they run on the mock model + synthetic data by design. No PR is merged; the frozen services stay untouched until each PR passes review + Sam's go (feedback-merge-gate).

What's inside

  1. POC results — all three branches green
  2. Ground truth — what the code actually is
  3. Two corrections the grounding forced
  4. Change-set 1 — oll-write: three JD-aware verbs
  5. Change-set 2 — the oll.in BFF + frontend
  6. Change-set 3 — foto: unstub the real model
  7. Change-set 4 — Core: guest pair, Radar price, event dedupe
  8. Change-set 5 — oll-scout, the one new service
  9. The POCs as runnable specs
  10. Env matrix — the new variables
  11. Parallel lanes & what I do on green

Ground truth read from the running services

Before writing a line of the plan, we read the three services oll.in leans on. This is the technical picture — real modules, real function names, real boundaries. Everything downstream is a diff against this.

ServiceLocationShape that matters
oll-write
LIVE
services/oll-write/Flat ~14 modules. Verbs are thin @write_bp.post("/<verb>") in routes.py; each calls service.<verb>(). Shared gate _authorize_and_gate(text, token)core_client.get_me → live plan → usage limit. Shared model call _run_model(messages)text_client.complete_full. Prompts = named constants in llm_client.py. Boundaries: text_client.py (oll-model, X-Service-Token) + core_client.py (Core JWT). No memory/RAG anywhere.
foto-serviceservices/foto/Guest checkout (no auth). replicate_client.py = provider registry: mock (default, no spend) vs replicate (guarded two-stage flux-LoRA train→sample). Idempotency = jobs.py SQLite claim_for_generation atomic UPDATE. Generation inline. Calls Core's guest billing pair (PR #21).
Core billing
LIVE
core/modules/billing/Routes all @require_auth except /webhook. service.create_checkout_session: mode = "payment" if plan=="one_time" else "subscription"both modes exist. Webhook dispatch table handles checkout.session.completed, customer.subscription.*, invoice.payment_*. No event-id dedupe table — relies on naturally-idempotent upserts.

Two corrections the grounding forced why we verify first

Correction 1

oll-write is stateless — grounding lives in the BFF

  • oll-write has zero memory/RAG integration; its only boundaries are oll-model + Core.
  • So "written from your real work, cited" cannot come from the verb alone.
  • The oll.in BFF does retrieval (from oll-memory, like ollwrite) and passes assembled, cited context into the verb as input.
  • Keeps oll-write frozen & stateless — no new boundary, matches the existing verb pattern (verbs take text in).
Correction 2

Core already does subscriptions

  • Subscription mode is the default; one-time is the special case (plan=="one_time").
  • Webhook already handles customer.subscription.deleted + invoice.payment_succeeded.
  • So Career Radar billing is reuse, not a new mode — the work is a Radar price/plan + turning on the subscription config.
  • The real gap: no event-id idempotency table. Add one before a paid recurring plan rides on it.
Net effect on scope: the "new backend" shrinks further. Career Radar billing moves from new to reuse + one small dedupe table. The dossier grounding moves from "oll-write feature" to "BFF orchestration" (reusing ollwrite's proven BFF). The genuinely-new service count stays at exactly one: oll-scout.

Change-set 1 — oll-write: three JD-aware verbs new prompts, not architecture

Copy the existing single-input verb pattern verbatim. Add tailor_cv, cover_letter, interview_prep. Each takes the BFF-assembled profile context (retrieved, cited) plus the job description. No new boundary, no memory client.

Files to touch

FileChange
routes.py3 new @write_bp.post("/tailor-cv" | "/cover-letter" | "/interview-prep"), each mirroring the clarify handler: bearer → DTO validate → service.X()_map_service_error.
dtos.py3 request/response pairs. Request = {profile: str, job_description: str, tone?: str}; response = the standard {text, provider, model} (CV/letter) or {questions: [...]} shape for interview prep.
service.py3 functions, each: _authorize_and_gate(profile+job, token)llm_client.build_<x>_messages(profile, job, tone)_run_model(messages). Same exceptions (AuthError/OverLimitError).
llm_client.pyNew named constants: _TAILOR_CV_SYSTEM, _COVER_LETTER_SYSTEM, _INTERVIEW_PREP_SYSTEM + build_*_messages builders. Keep the honest-editor ethics block; add the Swiss-format + "ground on cited passages, abstain if thin" rules. Respect MAX_INPUT_CHARS.
openapi.yaml3 new paths + schemas (contract-first; Schemathesis gate stays green).
tests/test_tailor_cv.py, test_cover_letter.py, test_interview_prep.py mirroring an existing verb test (mock text_client + core_client).
Prompt contract (per llm-call-contract): the profile passages arrive with ids; the system prompt instructs the model to write only from them and to leave a gap rather than invent. Citations are rendered by the BFF (below), not the model — the model just references passage ids. This is what makes the output "from real work, cited" and honest on a thin corpus.

Change-set 2 — the oll.in BFF + frontend reuse ollwrite verbatim

The oll.in web app is a thin Next.js client with a server-side BFF, lifted from ollwrite's proven pattern (proactive-memory-sidecar). The browser never names a collection or holds a service token — the BFF derives everything from the Core JWT.

oll.in browser
Next.js · upload CV · paste JD · review · send
Core JWT cookie only
the BFF — server-side
/api/* route handlers
derive collection: mem:user:{id} from JWT · inject memory service token · orchestrate
HTTP (service token / JWT)
live
oll-memory
ingest CV · retrieve cited passages
live
oll-write
3 new verbs (context in)
foto
guest headshot order
frozen
Core
auth · billing · email
  1. Upload CV → BFF ingests to oll-memory with a stable per-doc id (edit-safe upsert).
  2. Paste / select a job → BFF retrieves top-k profile passages (relevance floor; abstain path if thin).
  3. Generate → BFF calls the 3 oll-write verbs with {profile: assembled cited passages, job_description}.
  4. Render → BFF maps passage ids back to inline citation nodes (drag-to-cite reused from ollwrite).
  5. Photo → foto guest-checkout (no login) for the Bewerbungsfoto.
  6. Pay + deliver → Core checkout (guest pay-once pack) → Core email delivers the dossier.

Change-set 3 — foto: unstub the real model config, not code

The scaffold, guest-checkout wiring, idempotent store and tests already exist and pass. Going live is configuration + one verification run, not a rewrite — the replicate provider is already implemented and guarded.

# services/foto/.env — flip provider + supply flux-LoRA config
FOTO_PROVIDER=replicate
REPLICATE_API_TOKEN=r8_…            # burned if ever exposed
FLUX_LORA_TRAINER=<owner/model:version>
REPLICATE_USERNAME=<dest for trained versions>
FOTO_PUBLIC_BASE_URL=https://<ch-domain>

The _require_replicate_ready guard refuses to run until all four are set, so a misconfigured deploy fails loud, not silently on the mock. The only open question is output quality for the Swiss Bewerbungsfoto — which is POC-B, a run, not a build.

Change-set 4 — Core: guest pair, Radar price, event dedupe additive, frozen-safe

  1. Merge the guest pair (PR #21). foto is already coded against guest-checkout-session + guest-verify-session; they're additive and never weaken the authed routes. Needs Sam's per-PR go + green CI (feedback-merge-gate).
  2. Add a Career Radar price/plan. Subscription mode already works — this is a Stripe price id + a plan entry, no new billing code. The pay-once dossier pack reuses plan=="one_time".
  3. Add event-id idempotency before a recurring paid plan rides the webhook. A ProcessedEvent(event_id PK, processed_at) table; the webhook short-circuits a seen event.id. Small, but do it first — it's the one real gap the grounding found (idempotent-paid-fulfillment). Verify with adversarial-reviewer — this touches the money path.
Frozen-Core discipline: every item here is additive. The ProcessedEvent table is a new migration, not a change to existing handlers; the guest pair is new routes; the Radar price is config. Nothing existing is edited — that's the rule for touching a frozen service.

Change-set 5 — oll-scout, the one new service everything else was reuse

The single genuinely-new backend. It copies the house service template (thin routes · sole-boundary clients · own Neon DB · contract-first · per-endpoint tests) and adds nothing exotic. POC-C settled its one hard unknown — the data source is Jooble (primary, only self-serve API with real Swiss coverage, GREEN ToS) + Arbeitnow (open, no-key supplement).

services/oll-scout/
  app.py  config.py  routes.py  service.py  dtos.py  errors.py
  jooble_client.py     # SOLE data-source boundary (POST jooble.org/api/{key})
  arbeitnow_client.py  # supplement, no key
  scoring.py           # calls oll-model via X-Service-Token (like text_client)
  store.py             # own Neon oll_scout: jobs + user matches (ADR-008)
  digest.py            # daily match digest -> Core /email/send
  scout_cron.py        # the always-on tick (inline; Dramatiq only if load demands)
  openapi.yaml  Dockerfile  requirements.txt  tests/

The tick (once per user per day)

  1. Pulljooble_client.search(keywords, location="Switzerland") + Arbeitnow page; dedup on source id / updated.
  2. Scorescoring.py asks oll-model to rank each role against the user's oll-memory profile (fit + gaps), via the internal service token.
  3. Store — upsert roles + per-user match scores into Neon oll_scout.
  4. Digest — the day's strong matches → digest.py → Core /email/send. Alert, don't apply.
Concentration risk (POC-C, flag before building): Jooble is effectively the single load-bearing source for real Swiss listings — every government source (Job-Room/SECO, EURES, German BA) is publish-only or ToS-forbidden, and LinkedIn/Indeed are RED. Validate Jooble's CH field quality & result depth with live calls in the POC before designing the digest around it. JSearch is an internal-only enrichment fallback (RED if ever republished — active Google↔SerpApi litigation).

The POCs as runnable specs each a literal test, parallel-able

POC-A · Grounded dossier quality master gate · start now

  1. On a feat/ollin-verbs branch, add the 3 verbs to dev oll-write (or, faster: hand-build the prompt and hit the existing /draft to validate value before writing the verb).
  2. Ingest one real CV into oll-memory (Sam's own).
  3. Run tailor_cv + cover_letter + interview_prep against 3 real Swiss postings (pulled by hand for the POC).
  4. Pass gate: Sam judges ≥1 output good enough to send under his own name; every claim traces to a cited passage; no invented facts.

POC-C · Scout data source master gate · start now

  1. Request the free Jooble API key; capture the storage/caching terms from the issuance email in writing.
  2. Throwaway script: POST jooble.org/api/{key} with {keywords, location:"Switzerland"}; also GET arbeitnow.com/api/job-board-api.
  3. Inspect field richness (title/company/location/snippet/salary/link/updated), result depth, and CH coverage quality.
  4. Pass gate: Jooble returns enough well-structured CH roles under compliant terms to feed a daily digest. (Research says GREEN — this confirms field quality & depth with live calls.)

POC-B · Bewerbungsfoto output parallel

Set FOTO_PROVIDER=replicate + flux-LoRA config; generate from Sam's phone snaps. Pass gate: output passes an eyeball test vs real Swiss Bewerbungsfoto standards, consistent across ≥3 subjects.

POC-D · Radar billing reframed — smaller than thought

Subscription mode exists, so this shrank: add a test-mode Radar price + the ProcessedEvent dedupe table on feat/core-radar-billing; drive a subscription checkout → invoice.payment_succeeded webhook → /me reports the plan; replay the same event.id and assert it's a no-op. Pass gate: plan flips once, replay is inert, all existing billing tests stay green (adversarial-reviewer).

POC-E · Agent loop end-to-end builds on the proven seam

Add the 3 verbs to feat/oll-mcp-thin; apply services/oll-mcp/docs/openclaw-wiring.md; run one real application through OpenCLAW over the --http seam (human on send). Pass gate: OpenCLAW returns a review-ready dossier. (Seam already proven both transports.)

Env matrix — the new variables min-env, max-defaults

ServiceNew varsDefault / note
fotoFOTO_PROVIDER · REPLICATE_API_TOKEN · FLUX_LORA_TRAINER · REPLICATE_USERNAMEDefaults to mock (keyless); real only when all set (guarded).
oll-scoutJOOBLE_API_KEY · SCOUT_DATABASE_URL · OLL_MODEL_BASE_URL · OLL_MODEL_SERVICE_TOKEN · CORE_BASE_URLOwn Neon oll_scout; scoring + digest reuse the service-token pattern.
CoreSTRIPE_PRICE_RADAR (+ existing pack price)Radar price id; subscription mode already on.
oll.in BFFCORE_BASE_URL · MEMORY_BASE_URL · OLL_WRITE_BASE_URL · FOTO_BASE_URL · MEMORY_SERVICE_TOKENServer-side only; browser holds the Core JWT cookie, nothing else.

Parallel lanes & what I do on green chunked, non-overlapping

The change-sets map onto independent lanes — different files, different services — so they parallelize cleanly, each behind its POC gate.

Lane α · Dossier valuePOC-A → CS1 + CS2

Prove grounded quality, then ship the 3 verbs + the BFF/frontend. The MVP core. Master gate.

Lane β · Scout feasibilityPOC-C → CS5

Confirm Jooble field quality, then build oll-scout. Master gate for Radar. Runs fully parallel to α.

Lane γ · PhotoPOC-B → CS3

One config flip + a quality run. Independent; can finish first.

Lane δ · BillingPOC-D → CS4

Guest pair + Radar price + event dedupe. Gated by merge-gate; smaller than first drawn.

Lane ε · Agent + presencePOC-E / POC-F → last

MCP loop reuses the proven seam; Postiz presence deferred (highest ToS care, lowest near-term revenue).

On a green light I start the two master-gate POCs in parallel, both on feat branches, nothing deployed: POC-A (add the 3 verbs to dev oll-write, ingest a real CV, run against 3 real postings → Sam's send test) and POC-C (Jooble key + a live-call spike confirming CH field quality & depth). Each reports a binary pass/fail. No CS build-out until its POC is green. This is the plan turned executable — every step names a real file, a real endpoint, or a real command.