BA AI Engineer & Full-Stack TypeScript Developer

Independent · Remote · Async by default · A working demo every week, not a timesheet

12y building software other people depend on daily
5 AI systems I built, running in production today
every email fires exactly once, crash or not
~4,300 junk errors filtered every week, so real alerts get read

What I cover

I work across the whole system, and inside your team

Most AI work gets split up: someone writes the prompts, someone else owns the backend, and the model becomes a black box neither side wants to touch. Things break in that gap. I close it — sometimes by building the whole column myself, more often by dropping into a team and taking the parts nobody else wants. Twelve years on the same one taught me the difference between owning a system and hoarding it.

01 / 05

The database, done properly

Schemas, migrations, indexes, and the locking that stops two workers from picking up the same job. A vector column is still just a column. It needs the right index and a dedupe threshold somebody actually chose.

WORKER-LEASE.TS
const lease = await db.execute(sql`
  update conversations
     set lease_owner = ${workerId},
         lease_until = now() + interval '5 minutes'
   where id = ${conversationId}
     and (lease_until is null or lease_until < now())
   returning id
`);

if (lease.rows.length === 0) return; // someone else holds it

one atomic statement — two workers can't both win, and a crashed one releases itself

02 / 05

Retrieval you can argue with

Every answer comes back with citations. So when one of them is wrong — and one will be — you can point at the exact chunk that caused it. That is the whole difference between debugging and guessing.

RETRIEVAL.TS
const rough = await vectorSearch(query, { k: 24 }); // over-fetch
const ranked = rerank(rough, similarityAndUsefulness);
const context = ranked.slice(0, 6);

return {
  answer: await generate(prompt, context),
  citations: context.map((chunk) => ({
    chunkId: chunk.id,
    source: chunk.source,
  })),
};

every answer carries the chunk that produced it — a wrong one becomes debuggable

03 / 05

Agents that can't wander off

Typed tools, validated output, and guards that throw instead of waving through an object that merely looks right. If an agent can reach the wrong customer's data, that is a bug in my code. No amount of prompting fixes it.

TOOLS.TS
const lookupKnowledge = tool({
  parameters: z.object({ query: z.string() }),
  execute: ({ query }) =>
    searchKnowledge(query, {
      audience: "customer", // hard-coded, never a model input
    }),
});

// the filter runs in the SQL predicate, not in a prompt

the model cannot ask for internal rows — the boundary is code, not an instruction

04 / 05

What happens when it breaks

Kill a worker halfway through a job and your customer still doesn't get the same email twice. Retries that don't duplicate, locks that let go after a crash, alerts that stay out of the way. Nobody demos this. It's why the rest stays up.

SEND-STEP.TS
try {
  const res = await mail.send(confirmation);
  if (res.ok) await markSent(threadId); // only on a real 2xx
} catch {
  return ok(); // no retry inside this run —
}              // the next sweep re-attempts anything unmarked

a duplicate email is visible and survivable; a silently lost one is neither

05 / 05

Proof that it worked

On one project, corporate link scanners were inflating the visitor count by five to ten times and nobody had noticed. So I name the events before launch, write down what each one means, and strip out the traffic that was never human. Otherwise we're shipping a feeling.

ENGAGED-VISITORS.SQL
select count(distinct visitor_id)
from events
where name = 'page_leave' -- scanners never fire this
  and ts >= now() - interval '7 days';

count what a bot structurally can't fake — bot-resistant by construction, not by blocklist

Selected work

Five systems with real users on them right now

All five belong to one client's system: three parts of the loop, the measurement layer under it, and the surface that makes it legible to assistants. Three write-ups are published — how the pieces fit together, the analytics including the traffic that turned out not to be human, and what ChatGPT and Perplexity read when they arrive.

Flagship · production

The Brain

Two AI agents share one brain. It holds what the company knows, answers with citations, and takes in new knowledge through a review pipeline — so I can change how both agents behave without shipping a deploy.

  • Next.js 16
  • Mastra
  • AI SDK v6
  • MCP SDK
  • Drizzle
  • Postgres + pgvector
  • Neon Auth

Production · customer-facing

Dual-agent travel assistant

One chat window, two specialized agents behind it, each picking up the parts of the conversation that belong to it. When the brain feeding them goes down, the chat keeps working anyway.

  • Next.js 16
  • Mastra
  • AI SDK v6
  • React 19 + Compiler
  • Neon
  • PostHog

Production · the plumbing

Crash-safe automation backend

Kill the server halfway through a job and nothing gets sent twice, nothing gets lost, and the lock releases itself. Nobody puts this in a demo. It's the reason everything above it stays up.

  • Next.js 16
  • Workflow DevKit v4
  • Neon
  • OAuth2 integrations
  • SendGrid

Production · analytics

Product analytics & measurement layer

Their visitor numbers were running five to ten times too high — corporate link scanners the vendor's own bot filter never caught. I rewrote what every event means, rebuilt eight funnels, and got conversion measured against a denominator that's actually true.

  • PostHog
  • HogQL
  • Next.js 16
  • Event contract design
  • MCP
  • Mastra

Flagship · production

AI-visibility layer (AEO / GEO)

Making a content site legible to the assistants people now ask instead of searching — typed structured data on every page type, machine-readable endpoints, a named AI-crawler policy, and a frozen prompt panel that measures whether any of it worked.

  • Next.js 16
  • Schema.org / JSON-LD
  • llms.txt
  • Sanity
  • Technical SEO
  • IndexNow

All case studies

How it works

You'll know the price before I write any code

  1. First, a call about the problem

    Tell me what's wrong, or what you want to build. I'll tell you what would actually fix it — that might be AI, it might be plain engineering. And we agree up front what "better" looks like, so we can both tell afterwards whether it worked. Half an hour, no pitch.

  2. A written scope with a fixed price

    What I'll deliver, where the edges are, and what's deliberately left out. You approve a document — not an estimate that quietly grows once we're three weeks in.

  3. Then we ship in slices

    The first slice goes live early and small. You get to watch real people use it before the rest of the budget is committed.

  4. Handover — and I stay reachable

    Decisions written down, tests around the parts that matter, and a team that can change things without waiting on me. Handover means you're not locked in. It doesn't mean you're on your own — I'm still there when the thing I built needs me.

FAQ

The questions that come before a call

Five things people ask before they write. If yours isn't here, it's the thing to put in the message.

What exactly do you build?

Systems where a language model is one component rather than the whole product. In practice that means agents with typed tools, retrieval that returns citations you can check, the database and schema underneath, and the reconciliation and alerting that keep it running when something upstream fails at 3am.

The last part is most of the work and it is the part nobody demos. A chat feature that answers well on a good day is a week. One that still hands the right information to the right person after a crash, a timeout and a bad deploy is the actual job.

Do I need to know already that AI is the right answer?

No, and it is better if you haven't decided. Tell me what is slow, expensive or unreliable, and part of the first conversation is working out whether a model belongs anywhere near it.

Sometimes the honest answer is a queue, an index or a form that asks two fewer questions. I would rather say that on a call than discover it three weeks into a build you paid for.

If you disappear, is my team stuck with a system nobody understands?

That is the right thing to worry about, and it is the reason handover is a phase rather than an afterthought. Decisions get written down as decisions — what was chosen, what was rejected, and what would have to change for the choice to be revisited — with tests around the parts that would be expensive to get wrong.

The measure I hold myself to is whether your own developers can change the thing without waiting on me. Staying reachable afterwards is separate, and priced separately, so it is a choice rather than a dependency.

How do you charge?

Fixed price against a written scope, or a monthly retainer for a set slice of my time. Not hourly — an hourly rate turns the conversation into where I live rather than what the system is worth, and it rewards the wrong thing on both sides.

Support after launch is quoted up front the same way, so it is a line you agreed to rather than an invoice that arrives as a surprise.

How long before I see something real?

The first slice goes live early and deliberately small — narrow enough to be genuinely finished, real enough that actual people use it. You get to watch that happen before the rest of the budget is committed.

It is also the cheapest way to find out we disagree about something, which on this kind of work is usually a definition rather than a technology.

Next step

Tell me what needs to work

My agent picks up first. It knows the projects, the stack and what I'm free for, and it gives you a straight read on whether this is a fit — including when the answer is no. Everything it hears reaches me.