[AI Infrastructure, Platform Engineering]

Unified AI Gateway for App Builders

A platform that combines model routing, tracing, cost analysis and more — so builders like me can drop AI infrastructure into any app instead of rebuilding it every time.

your apphow can I help?your appONEFOLDone endpointmessages · modelsession · trace idONEFOLDresolve routeanthropicopenaigoogleyour appONEFOLDanthropictrace recorded0 tokens · $0.000001 YOUR APP

Role

Sole engineer

Architecture, build, operations

Scope

Platform architecture

Protocol translation

Edge runtime and streaming

Developer experience

Telemetry pipeline

Platform infrastructure

Stack

TypeScript on Cloudflare Workers

Postgres 18 via Hyperdrive, ClickHouse

k3s on Hetzner, OpenTofu, CloudNativePG

React console, Astro catalog

[Problem Space]

Three apps, and the same scaffolding built three times

I shipped three apps this year. Two of them lean on models for the part that makes them worth using.

Every one started with the same checklist, and none of it was product work. Pick a provider and install their SDK. Paste the key into another secret store. Bolt on something to see what the model actually returned, because the logs only tell you it responded. Wire up a harness for tool calls and retries. Build another chat UI. Add cost tracking, always right after an invoice surprised me.

Days of work before a single line of the thing I actually wanted to build.

The tools meant to fix that came with their own tax. Wire up an observability platform, spend an afternoon configuring it, then open the dashboard and still not be sure what I'm looking at. I wanted one number — what a session costs me on average — and getting it meant learning someone else's schema first.

Evals were worse, because mostly I skipped them. A better model ships every few weeks. I had no way to check whether the one running in my app was the right pick for the job it was actually doing. Not better on a leaderboard. Better at this. Every app I own is running a model I picked on day one and never checked again.

Onefold is that scaffolding: built once, reused by every app in the fleet.

[Solution]

AI gateway with tracing and experiments built in

Apps point at one endpoint and speak one dialect. The gateway resolves the model, rewrites the request into whatever that provider actually speaks, streams the answer back untouched, and records what it cost. A thin SDK carries a session and trace id with every call, so a conversation is one object in the console rather than a pile of unrelated requests — and swapping a model becomes an experiment you can measure instead of a guess.

Onefold SDK session + trace id Worker key → org, balance catalog row → route + price spec for that dialect translate body + headers provider one event, after the response ctx.waitUntil — never blocks the caller The response streams back through the same path in reverse, translated frame by frame.
One request, end to end. Routing and budget checks are synchronous. Accounting runs after the caller already has their answer.
[Platform Infrastructure]

Optimized for scalability at the lowest cost

A scheduled job polls every provider in the catalog on a fixed interval and records uptime, error rate and latency alongside the request events. That's what lets the console call a provider degraded instead of guessing, and what keeps a routing decision defensible after the fact.

Measuring upstream availability sets the bar for our own. Routing reads the catalog on every request, so the control plane has to survive losing a machine or the gateway stops admitting traffic entirely — and a layer that grades its providers on uptime has to hold the same standard.

The managed version of this is RDS Multi-AZ, EKS and a managed stream — roughly four hundred dollars a month to protect a system with no paying users. The same guarantees on Hetzner cost thirty-one. That meant six decisions:

Compute

Three nodes in Falkenstein, Nuremberg and Helsinki, on one private network with a Hetzner load balancer in front.

Orchestration

k3s. Scheduling, rolling deploys and self-healing in a single binary, with Hetzner's cloud controller and CSI driver wired in.

Database Failover

CloudNativePG runs a primary and two replicas, synchronous commit to at least one. Promotion completes in under thirty seconds.

Backups

WAL ships to R2 as it's written. Thirty days of point-in-time recovery. A restore drill runs monthly into a scratch namespace.

Infrastructure as Code

OpenTofu owns the servers, network, firewall rules, load balancer and DNS. State lives in R2. A full rebuild takes twenty minutes.

Edge Degradation

The routing catalog is cached at the edge and served stale on error, so losing every node degrades prices instead of refusing traffic.

ClickHouse stays single-node. Analytics is append-only, off the request path, and rebuildable from the staging table, so replicating it would double the cost to protect data nobody is waiting on. It snapshots to R2 nightly.

Three nodes at €22.65, load balancer at €5.39, volumes at €1.90, R2 for about a euro. Thirty-one a month. Recovery point under a minute, recovery time under five.

Once a month I delete a node from the CLI without warning and watch the cluster pick it up.

Cloudflare edge gateway worker console worker catalog cache stale-while-revalidate Hetzner LB fsn1 — primary Postgres rw · ClickHouse · drain nbg1 — replica sync commit hel1 — replica async Cloudflare R2 continuous WAL · 30-day PITR · zero egress k3s · one private network zone WAL shipped continuously Whole fleet declared in OpenTofu. Lose any node and a replica is promoted; the read-write service moves and Hyperdrive follows it. Lose all three and the edge keeps routing on a cached catalog. €31/month, all in.
Three data centers, one private network, and an edge that keeps serving when all of it is gone.
[The Adapter Engine]

What adding a provider actually requires

Providers diverge in four places: the shape of the message list, the names of the parameters, the event protocol on the stream, and the path where the error message sits. A module per vendor encodes those four differences once per vendor, and the modules drift apart as they are maintained — a fix applied to one is a fix missing from the others.

So a provider is not code here. It is a declarative spec the engine executes. The spec names the auth headers with a placeholder where the secret goes, a message style and a stream style by name, a map of parameter rules, and dot paths into the provider's error body.

Parameter rules cover four cases. Rename a field. Wrap a scalar the provider only accepts as an array. Fill a required field from the catalog row when the client omits it. Or hand the value to a named translator when the shape has to change rather than the name.

The engine reads that and does the work. The entire Anthropic provider is nineteen lines and contains no logic.

export const anthropic: Spec = {
  dialect: 'anthropic',
  auth: { 'x-api-key': '{key}' },
  headers: { 'anthropic-version': '2023-06-01' },
  messages: 'system-hoisted-blocks',
  params: {
    max_tokens:  { to: 'max_tokens', default: 'maxOutput' },
    temperature: { to: 'temperature' },
    stop:        { to: 'stop_sequences', wrap: 'array' },
    tools:       { to: 'tools', transform: 'tools-to-anthropic' },
  },
  stream: { flag: 'stream', style: 'anthropic-events' },
  error: { message: 'error.message', type: 'error.type' },
};

Config stops where the behavior is genuinely new. A provider whose message shape and stream protocol match an existing style is a data change: add the spec, add the catalog rows, ship. A provider speaking a protocol nothing else speaks needs a new stream style, which is code in the engine.

So the second provider on a known protocol is close to free and the first on a new one is not. That is the intended property — the cost sits where the novelty is.

The spec — data auth headers, with {key} placeholder message style, by name param rules: to · wrap · default · transform stream style, by name error paths, as dot paths The engine — code messageStyles — reshape the turns valueStyles — reshape a value streamStyles — parse the events built per request when stateful points at by name A provider that reuses existing styles is nineteen lines of configuration and no new code. A genuinely new protocol still needs one new style — the claim is bounded, and worth stating.
The spec is data. The styles it names are code. New vendors add the first kind. New protocols add the second.
[Tracing]

What a trace is actually made of

A trace is not one record. A single turn in an app fans out into several model calls — a plan, a tool call, a summarization — and the useful question is almost always about the group rather than the call. So the SDK carries three identifiers: a session id that lives as long as the conversation, a trace id for one logical operation, and a request id per model call. The gateway validates them, bounds their length, and stamps them onto the event it records.

The record itself is split by access pattern. Metadata — the three ids, model, provider, status, finish reason, token counts, latency and cost — is a narrow row of roughly two hundred bytes in ClickHouse. The bodies, meaning the prompt, the response and any tool arguments, go to R2 under a key derived from the request id. The row stores the key and nothing else about the content.

That split follows from how the data gets read. Aggregations scan millions of rows and never open a body. A trace view reads one body at a time, and only when someone expands a call. Keeping bodies in the columnar store would make every scan carry bytes nobody asked for, and object storage costs roughly an order of magnitude less per gigabyte than analytic storage does.

The table is ordered by organization, then session, then time, so every call in a session is contiguous on disk. Rendering a trace is one range scan rather than a search, and it stays one range scan as the table grows.

None of this sits on the request path. Both writes happen after the response has been handed back — the row through the staging table and the minute drain, the body straight to R2 — so a tracing failure costs a log line rather than a call. For a streamed response the row is written when the stream drains, which is why latency measures generation rather than time to first byte, and why a stream that breaks mid-flight still produces a trace instead of a gap.

Cost is stamped on each row at request-time prices, so average cost per session is a sum over a contiguous range. That is the question I could not answer before any of this existed.

Onefold SDK session_id — one conversation trace_id — one operation request_id — one model call gateway validates and stamps the ids ClickHouse row — ~200 bytes ids · model · tokens · latency · cost · R2 key ORDER BY (org_id, session_id, occurred_at) R2 object prompt · response · tool arguments 30-day lifecycle rule Console trace view key one range scan per session body fetched only when a call is expanded after the response Metadata is small, ordered and kept. Bodies are large, rarely read, and expire on their own.
Three ids from the SDK, one narrow row, and the body it points at.
[Experimentation]

Comparing models on traffic you already ran

Model choice in most apps is a decision made once, on the day the feature shipped, and never revisited. New models arrive every few weeks, and the public benchmarks measure general capability, which is not the question anyone actually has. The question is whether a particular model is better at the particular job one app gives it.

The compare screen answers that with traffic that already happened. Pick a trace, or a saved set of them, pick two or three models, and the playground replays the recorded request against each. Output, token counts, latency and cost land side by side.

This is only cheap because of the storage split. The full request body is already in R2, keyed by request id, so a replay reconstructs nothing — it reads the recorded payload and sends it again. It sends it through the same adapter engine the gateway uses in production, so a comparison exercises the identical translation path rather than a separate harness that can drift from it.

Replays are marked at write time and carry a flag through to ClickHouse, so experiment traffic never lands in production cost rollups or latency percentiles. They still bill against the same key and the same balance. A model comparison that hides its own cost is not much of a comparison.

A group of traces can be promoted into a fixed set — twenty representative conversations, say — and rerun whenever a new model ships. The set stays constant, the model changes, and the diff is the only thing left to read.

What the screen does not do is grade. There is no score and no automatic winner: outputs sit next to each other and a person decides. An automated judge is the obvious next step and is not built.

[The Whole Thing]

Every piece, in one path

One diagram for the system described above. Solid is the request path. Dashed is everything that happens after the caller already has their answer — the event row, the payload body, the health checks and the drain.

user request your app Onefold SDK session + trace id Onefold gateway authenticate · check budget resolve model → route + price translate · stream · meter Anthropic OpenAI Google health cron uptime · error rate · latency Postgres 18 catalog · keys · budgets · staging drain job systemd · 60s ClickHouse metrics + R2 object key Cloudflare R2 prompt + response bodies Console traces · spend · latency tokens stream back, translated frame by frame catalog read on the request path one event, after the response ctx.waitUntil — never blocks the caller payload body written once, keyed key k3s cluster Solid is the request path. Dashed happens after the caller already has their answer. The row in ClickHouse stays small; the body it points at lives in R2.
[See More Work]

See the next project ↓

[Systems, Full-Stack]
Browser SPA Phone Worker · Hono session gate /api/t/* · /timer/* · /chat Hyperdrive Postgres Mac daemon launchd · OBS daemon API secret-gated R2 recordings, 60d cookie pooled secret multipart reconcile Dashed lane: recording path. Solid lane: the request path a page load depends on.

Chamber

A task system that keeps the estimate and the actual, and shows you the gap.

[AI Product, Founder]
atlas.finneykoshy.com/companies
Atlas CRM
Quick actions /

Records

People
Companies
Agents
Logs
Projects
Pipeline
Analytics
Finney Koshy

Companies

71
∞ Find companies Show closed (89) + Add
≡ Sort ≡ Filter
Company Stage Industry People Notes Added
OpenAI
Reply received AI / Foundation Models
SA GB
ChatGPT, GPT-4, Codex May 9
Anthropic
Intro booked AI / Foundation Models
DA
Claude, Constitutional AI May 9
Stripe
Message sent Fintech / Payments
PC JD
Online payments infra Apr 21
Vercel
Interviewing Dev tools / Infra
GR
Frontend cloud, Next.js Apr 10
Linear
Reply received Dev tools / PM
KH
Issue tracking for SaaS Apr 21
Figma
Person identified Design / Collab
DF
Collaborative design tool May 9
Notion
Message sent Productivity
IZ
Docs, wikis, projects Apr 10
Airbnb
Message sent Marketplace
BC
Marketplace for stays Apr 21
Shopify
Person identified E-commerce
TL
E-commerce platform May 9
Ramp
Reply received Fintech / Cards
EG KM
Corporate cards & spend Apr 10
Discord
Message sent Comms / Social
JV
Voice, video, chat Apr 21
Spotify
Reply received Consumer / Audio
DE
Music streaming Apr 10
Coinbase
Message sent Crypto / Fintech
BA
Crypto exchange Apr 21
Plaid
Person identified Fintech / Infra
ZP
Banking API infrastructure May 9

Atlas

A career CRM that runs the job search like a sales pipeline.