oll.am · Product Service · scaffolded 2026-06-28

foto-service

The AI-headshot product — the platform's next dollar. The first product built on the live Core: it owns no auth, no Stripe, no email. It calls Core over HTTP and generates with Replicate. A working scaffold (12 tests green), not yet deployed.

What it is

A Swiss job-seeker uploads selfies, pays CHF 49 once with no account, and gets a set of professional Bewerbungsfotos emailed back. That's the whole product. Everything that isn't "generate headshots" — login, payment, email — belongs to Core. foto-service is ~the product logic and three HTTP calls.

It's the proof that the locked architecture pays off: a new revenue product is a small, disposable service, not another monolith. The LoRA generation pipeline already exists and works (13+ models trained) in the wardrobai codebase; foto-service is the thin Core-client shell around it.

The guest flow

landing ──► POST /api/foto/checkout {email, tier, style} │ foto creates a job_id, stores a pending job (SQLite), │ maps tier → Core product (pro → headshot_chf49, plan=one_time) ▼ Core /api/billing/guest-checkout-session (NO auth) │ foto OWNS success_url = {FOTO_BASE}/fulfill?job_id=... │ Core appends &session_id=cs_... ──► {url} ▼ Stripe hosted checkout (email captured at Stripe) ── pay CHF 49 ▼ /api/foto/fulfill?job_id=...&session_id=cs_... │ Core /api/billing/guest-verify-session → trust paid == true │ idempotent on job_id · atomic claim before any paid work ▼ Replicate (LoRA generate) ──► results-ready email (links) ──► done (order-confirmation email is sent by Core's webhook, PR #21)

Identity is the order, not a user. foto's own job_id rides in the success_url it controls, and Core appends the Stripe session_id — so the return hits /fulfill?job_id=…&session_id=… with both. No account, no JWT, no plan to flip, no fragile URL-parsing. The product trusts only a server-side guest-verify-session, never the browser.

The house patterns it demonstrates — specview reference + the skill library

FilePattern / skill
core_client.pySole HTTP boundary to Core — module-level create_checkout/verify_session/send_email, one auth-header helper, explicit timeout, raise_for_status, narrowed typed return (http-service-adapter)
replicate_client.pySole boundary to Replicate — returns list[str] URLs only, never raw SDK objects (http-service-adapter)
service.pyverify → generate → email, idempotent on session_id (guest-checkout-grant + inbound-webhook-handler discipline)
jobs.pyOn-disk SQLite order store (Neon is Core-only per the data decision) (local-first-sqlite-store)
errors.py{code, message, request_id} envelope mirroring Core (error-envelope)
routes.py / dtos.pyThin DTO-validated blueprint (the house reference template)
openapi.yamlSpec slice for the foto endpoints (contract-first-api)
app.py / DockerfileBoot env-gate (fail fast), 12-factor $PORT, non-root, HEALTHCHECK over 127.0.0.1 (dependency-health-probe)
tests/boot-gate · checkout persists job · idempotent fulfill · unpaid does no work · health — Core + Replicate mocked. 12/12 green.

What blocks the first real sale

✅ The two guest blockers are solved by PR #21 (additive, awaiting greenlight)

1. Billing auth-wall → solved. PR #21 adds guest-checkout-session + guest-verify-session (no auth, no ownership gate; verify returns paid, email, amount_total, metadata). foto is already wired to them — checkout + payment verification work with no account.

2. Order-confirmation email → solved. PR #21 sends the guest order-confirmation from Core's webhook (service layer, sidestepping the HTTP route's recipient scoping). foto no longer sends that one.

One gap remains — the results-ready email (post-generation image links)

The headshots are generated after payment, so Core's webhook can't carry them. foto's results-ready email still hits Core's authed, recipient-scoped /email/send, which 403s for a guest address. The code sends it, logs the failure, and still marks the job delivered (the /fulfill response returns image_urls so the frontend can show results directly). Fix = the PR #21 trusted-service bypass on /email/send, OR a foto-owned Resend sender. Your decision.

Run & test it

cd services/foto python3 -m pytest -q # 19 passed cp .env.example .env # set CORE_BASE_URL, FOTO_PUBLIC_BASE_URL, REPLICATE_API_TOKEN PORT=5001 python3 app.py # GET http://localhost:5001/api/foto/health → {"status":"ok"}

Tests mock Core + Replicate, so they make no network calls and need no secrets.

Stubbed / TODO (honest)

oll.am · foto-service · scaffold for review, not deployed · Overnight Plan · Live status