Work

Anabolic

Full stack engineer, Oct 2024 to Apr 2025

Anabolic is a fitness influencer brand that sold training programs and meal plans to natural athletes, and ran Enhanced Snus alongside them. I joined in Year 12 as its only full stack engineer, which meant the web, the backend and whatever broke mid-sale. I also worked with the content team so the sites matched the marketing.

Enhanced Snus

Oct 2024 to Apr 2025enhancedsnus.com
Sales in waves: long quiet stretches, then a surge.

Keeping a high-volume store up while traffic, stock and checkout kept changing.

Role
Full stack engineer
Period
Oct 2024 to Apr 2025
Site
enhancedsnus.com
Built with
WordPress, Elementor, WooCommerce

The site

An online shop for nicotine pouches: fourteen brands, sorted by brand and by strength, with bundles, an affiliate programme and a strength guide. Black and minimal. I came in after the site was mostly built and maintained it for the business owner.

The home page as archived in July 2025. A static copy: links and the cart are switched off.

Keeping it up

It rarely went down on its own, but traffic arrived in bursts, spiking whenever a new piece of content went out, and handling that load was the job. Concurrent checkouts for the last unit of a product created stock race conditions, and the existing plugins couldn't handle them, so I built that handling myself. Stock also changed constantly, so product pages lagged behind actual availability. Payments, oddly, were the calm part.

The site had started as a poorly maintained WordPress install. Elementor didn't have the blocks it needed, and the AI help around at the time, Gemini 2.5, couldn't make up the difference, so I scavenged it back together with hand-rolled CSS and HTML. That's how I learnt to run a website: by keeping this one alive.

Result

The site brought in six figures of revenue while I worked there.

The anabolic.au build-out

Oct 2024 to Apr 2025anabolic.au
Scattered pieces settling into a grid: where programming clicked.

The first site I built from scratch, and where programming clicked.

Role
Full stack engineer
Period
Oct 2024 to Apr 2025
Site
anabolic.au
Built with
WordPress, WooCommerce, Shopify

How I got into programming

I built the web presence for anabolic.au from the ground up, first on WordPress and WooCommerce, then on Shopify. It looks quite easy in retrospect, but at the time I was so new to all of this that every step was a first, and it's where programming started to click for me.

The home page as archived in October 2025. A static copy: the carousel, cart and forms are switched off.

Result

The store brought in five figures of revenue while I was there. It sold different kinds of product side by side: direct digital downloads, and physical merchandise that had to be shipped. I set up the store's architecture so each type was handled properly, from the product page through checkout to delivery.

Anthrolytic

COO and CPO, Feb 2026 to Sep 2026

Anthrolytic is trying to replace the resume. Its product, VirtualShift, puts a candidate through a short simulated shift, talking to an AI customer or coworker, and gives the hiring team a shortlist ranked on what people actually did. I co-founded it with Richard Day and Simeon Goldberg. I was COO and CPO; in practice I ran product, deciding what we built and how it was designed, and I helped Richard with the research.

In 2026 we went through Antler's AUS16 residency, one of 83 founders picked from more than 2,000 applications. It showed me how hard I'd need to work, and how cheap an idea is next to the work of making it real.

The Anthrolytic website

Feb 2026 to Sep 2026
Resumes thrown in the air, one picked out.

Our front door and honestly one of my Hall of Fame design streaks.

Role
COO and CPO
Period
Feb 2026 to Sep 2026
Built with
Next.js, React, Tailwind, Vercel

Iterations

The first version launched in February: near-black, a purple gradient, and a WebGL hero where crumpled resumes fly past the camera as you scroll. It began as a scrounged-together Lovable build, and it looked very vibe coded, though I kind of liked the vibe-coded colours. I was overwhelmed by the hype of being able to design like that at the peak of the vibe coding era. I soon sobered up to the reality: it looked very SaaS, B2B, LinkedIn hype, and it needed fixing.

In April I redesigned it in light mode: a soft purple palette, a serif headline, “Hire the person, not the resume”, and every section and sub-page converted.

The flying papers stayed, but rebuilt without 3D, much more efficiently. The WebGL version was too complex to load on many devices, phones especially. It's now 32 resume cards on CSS transforms, scrubbed by scroll on a sticky stage. Positions update through refs in an animation frame rather than React state, scroll listeners only attach while the hero is near the viewport, and only the first eight images load up front.

The current Anthrolytic site in light mode: the headline “Hire the person, not the resume.” over a stack of resumes.

Now: light

The earlier Anthrolytic site in dark mode: “Hiring, reinvented” over a 3D stack of papers on black.

Before: dark

The redesign, April 2026.
The resume stack bursting apart as you scroll.
The original: crumpled papers in WebGL, on the dark site.

Pivot

We had just pivoted, and what I wanted to show, artistically, was that we were a quality recruitment agency: one that could sort through the field. Before, the pitch had been that we could find out a lot about a person without their resume. While the full site is held back, the home page is a single question: “Who are we hiring today?” You type a role in your own words, it pins your brief and “starts reading it”, and a field of dots appears, one per person, with an agent moving through them looking for the right one. Then it asks your name, email and phone one at a time, each answer folding away as you go.

But as much as it looked and felt like an agent, it was simply a waitlist sign up. The waitlist page uses the same field: it was the original application for it, and the agent roams, pauses, and locks onto a single dot. I was inspired by a meme of Rick from Rick and Morty walking through a field.

Typing a brief, then the agent searching the field.
The hiring intake after submitting a role: the brief pinned at the top and a field of dots with a glowing agent scanning it.

Intake

The dark waitlist page: a dot field with a roaming agent and an email field.

Waitlist

Rayon

Feb 2026 to Sep 2026
Two voices taking turns, with the transcript running between them.

An interview co-pilot that suggests follow-ups and scores against a rubric, for fairer interviews.

Role
COO and CPO
Period
Feb 2026 to Sep 2026
Built with
Electron, Next.js, TypeScript, AWS Cognito, Playwright

The product

Rayon was an interviewing co-pilot. It helped interviewers go in ready with questions that dug deeper, and understand applicants better, so that everyone got a fairer interview. A reverse Cluely, if you will.

It sat in on a job interview. The interviewer ran a desktop app that listened, told the two voices apart, and either suggested follow-up questions as it went (copilot) or took notes quietly (intern). When the interview ended it scored the candidate against the role's rubric. The hiring team reviewed scores, transcripts and rubrics in a web dashboard afterwards.

I owned the entire user flow, and Richard built the backend. The desktop app was mine: 186 of its 220 commits came through me, written by coding agents I directed and reviewed. It was my first Electron app and the first time I'd streamed live audio anywhere. We pulled two all-nighters back to back to get it to our clients on time, and still didn't make that morning; it was more complex than we'd planned for. The Rust API and the infrastructure were Richard's.

User experience

The live screen has to work while someone is mid-conversation, so it's three calm panels: notes, suggested questions with the transcript, and the role's context with audio meters. Every line is labelled interviewer or candidate, and one click corrects a wrong label, because a score built on misattributed answers is worse than none.

Two things mattered most to me here. It had to be glanceable: someone mid-conversation should be able to take in the whole screen in a second without losing the person in front of them. And it had to be easy to know what question to ask next, so the suggestions were short, clear and written to be friendly and easy to understand for both sides of the conversation, the interviewer reading them and the candidate hearing them.

Consent comes first: nothing streams until the candidate has agreed, and withdrawing stops the stream on the spot. A dropped connection shows plainly and resumes where it left off.

In the dashboard, AI-drafted rubric criteria are never saved without a person, and the editor locks while a draft is generating so a late result can't overwrite someone's edits.

Rayon dashboard showing a candidate's record with tabs for score, transcript, notes and audio, and a rationale for each scored metric.
A candidate's record: each score comes with its reasoning. (Demo data.)
Rayon rubric editor with a role brief, a button to generate a draft, and the list of metrics.
The rubric editor. Drafts from the model are suggestions until someone saves them. (Demo data.)

Speeding up the dashboard

The dashboard's server was making one call to the API per interview, plus a sign-in check in front of every one of them. With 150 interviews, the overview took over three seconds to load.

So I hill-climbed it: measured every API call, cut or cached whatever I could, measured again, and kept going. With the data warm, the dashboard call went from 3.2 seconds to 24 ms, and moving between pages stopped flashing a loading screen. The very first load still pays the full price, because the real fix is a bulk endpoint in the API, which I wrote up for it.

Rayon dashboard overview with summary tiles, recent candidates and interview activity.
The overview: the page that used to take three seconds. (Demo data.)

The little guy

We felt the app was a bit boring, much like the rest of HR, so Rayon had a little character who ran around it while it worked. People did not react well: he was distracting, and by the end we didn't like him either. He used far too much RAM, and in the middle of a live interview he was too heavy to load next to the real work. The real work came first, so he didn't make it to release.

What didn't work

We built Rayon as a one-off for a single client, and even for that one client it was too complex to hold up. Making it hold would have taken a lot more work than the demand justified, because in the end it wasn't that different from Granola. So we stopped, and took what we'd learnt into VirtualShift.

PersonaPlex post-training

Feb 2026 to Sep 2026With Richard Day and Simeon Goldberg
Listening and speaking at once: two sets of ripples crossing.

Teaching a speech-to-speech model to play the customer instead of the barista.

Role
COO and CPO
Period
Feb 2026 to Sep 2026
With
Richard Day and Simeon Goldberg
Built with
PersonaPlex-7b, LoRA, ChatterboxTTS, GPT-4o as judge

The question

NVIDIA's PersonaPlex-7b is a full-duplex speech model: it listens and talks at the same time, like a person on a call. It was built to be the service agent. We needed it to be the customer, so people could practise a shift against a convincing one.

Out of the box it kept slipping back behind the counter: barista phrases in the customer's mouth, talking unprompted the moment a call connected, and losing its persona halfway through.

“When conditioned to act as a customer rather than a service agent, the base model frequently reverts to barista-side language, generates unprompted speech on connection, and loses persona coherence mid-conversation.”
CoffeePlex, §1 Introduction

What we did

We fine-tuned it with LoRA on 200 synthetic coffee-shop conversations with angry, nervous and friendly customers. GPT-OSS-120B wrote them, Claude Sonnet checked them, and ChatterboxTTS gave them voices.

There are two ideas on top of the fine-tune. The first weights the loss 100× towards the model's semantic codebook, so training cares about what is said more than how the voice sounds. The second, voice prompt injection, plays the model a pre-recorded barista greeting before the first turn, so it starts the call already knowing which side of the counter it's on.

Diagram of the voice prompt injection pipeline: synthetic dialogues are voiced with ChatterboxTTS into a stored barista greeting, which is injected as audio context before the first turn.
Voice prompt injection: a stored greeting, voiced at training time, is played in as audio context before the first turn.
Table showing an example persona, its system prompt, and a synthetic training conversation between a customer and a barista.
One of the synthetic training conversations: “Chatty Chloe”, near closing time.

What we found

With no standard benchmark for full-duplex persona work, GPT-4o judged each conversation's transcript on a five-point scale. Against the base model, emotional accuracy went from 2.2 to 3.4 and task adherence from 2.6 to 4.2, while the hallucination score dropped from 3.6 to 1.8. The voice prompt alone accounted for a large share of that: on the fine-tuned model it cut hallucinations from 2.6 to 1.8.

It's a small study, and the paper says so: a handful of conversations per setup, and no ablation yet that isolates the semantic loss weighting.

Playing the customer got more convincing, with fewer made-up lines

GPT-4o judge scores out of 5, base model against the fine-tuned model with voice prompt.

12345Emotional accuracy+1.22.23.4Task adherence+1.62.64.2Hallucination-1.83.61.8Base modelFine-tuned
Base model to Fine-tuned
LabelBase modelFine-tunedChange
Emotional accuracy2.23.4+1.2
Task adherence2.64.2+1.6
Hallucination3.61.8-1.8

How to read: Grey is the base model, indigo is after fine-tuning. For hallucination, lower is better.

Source: CoffeePlex, §4.2 Results (Anthrolytic, 2026)

The voice prompt alone cut hallucinations by almost a third

Hallucination score on the fine-tuned model, out of 5. Lower is better.

12345Hallucination-0.82.61.8With voice promptWithout voice prompt
Without voice prompt to With voice prompt
LabelWithout voice promptWith voice promptChange
Hallucination2.61.8-0.8
Source: CoffeePlex, §4.2 Results (Anthrolytic, 2026)
“Hallucination Rate decreases from 2.6 to 1.8 (↓30.8%), suggesting that grounding the model with an acoustic prior suppresses off-persona responses.”
CoffeePlex, §4.2 Results

My role

I helped research the model, including working out that we could fine-tune it with LoRA instead of retraining it outright. Then I worked with Richard on the evaluation: deciding which benchmarks we needed, and running them.

Why it mattered

VirtualShift needed a voice that could play the customer, and PersonaPlex was the obvious place to start. We tried it as it came, and it couldn't do it: playing the opposite side of everything in its training data was beyond it. Our training was aimed squarely at breaking that habit, and without this research the product wouldn't have had a customer to talk to.

VirtualShift

Feb 2026 to Sep 2026
A room built from a few photos, and the desks that floated above the carpet.

The product behind Anthrolytic, and the pipeline I built so agents could build the places its shifts happen in.

Role
COO and CPO
Period
Feb 2026 to Sep 2026
Built with
Blender 5.2, Unreal Engine 5.8, MetaHuman, Python, PowerShell, Claude Code

My part

I worked on VirtualShift from the start, and on more than one side of it. On the product side I ideated on the concept and focused heavily on the user experience. On the 3D side I developed the environments for these experiences, working hands-on with agents.

The job

VirtualShift puts candidates into a realistic shift, so it needs realistic places for those shifts to happen. The first was the concierge lobby of a Southbank residential tower. I rebuilt it in Blender from reference photos, 414 meshes, then moved it into Unreal and spent a long time with MetaHuman getting 11 characters to carry seven scenes of about thirty seconds each. By the end the lobby looked great.

One lobby wasn't the point, though. Blender turned into a long and tumultuous build of a pipeline my Claude agent could run on its own, turning a couple of photos or a description into a working environment.

A frame from the lobby film: two MetaHuman characters in a residential lobby, one in a two-tone hi-vis shirt, with shallow depth of field.
Scene 4, from a very early build.

How it works

Blender models and bakes, and Unreal lights and renders. Agents drive both once I finish initial designs. Headless Blender exports through a single checked FBX path, and terminal and Python run Unreal through its Remote Control API, with Movie Render Queue as the only render path I trusted. One unattended overnight run rendered 68 variants in under five hours without a failure.

Walking the office floor, rendered in Unreal.

What went wrong

I often set guardrails for one run, before realising I had overprescribed, having to balance exact problems I was having with the fact I can't predict every collision the agent may run into. That was very new to me, because when I build in these environments alone, the flow state you get into removes the thought of every potential fault.

A rendered office workstation with two monitors on a desk.
A workstation from the office test.

Result

Right after the pipeline came together, Astra came out and could nail these scenes just as well on its own. Almost four billion tokens of work, overtaken. It's the most expensive lesson I've had in how fast this field moves, and I'd still rather have learnt how a 3D pipeline works than not.

Squidward staring blankly, one eye covered by a striped ball (via GIPHY)

How I build with agents

Jan 2026 to now
The loop: a few large frontier models, many small builders, a whiteboard at the centre.

I don't read the code. I make sure I understand the system, and the agents write it.

Role
Directing coding agents
Period
Jan 2026 to now
Built with
Claude Code, Codex, OpenCode, Hermes, OpenClaw, Grok

The loop

It starts with planning and ideating, and making sure I understand the structural decisions I'm making. I begin with an MVP of sorts, then iterate rapidly to get the smoothest experience possible. The tools I like to use for this include /visualize, /visualize plan, flowcharts, grill me, quiz me, and everything else from Claude Code. Credit for these goes to GitHub.

I don't read the code anymore

I'm a big believer that agents already write better code than I would. My job is to know the system well enough to survive being quizzed on it, to judge what comes back, and to say no. Reading every line wouldn't make me better at any of those.

When it went wrong

More times than I could count, but twice badly. One time an agent emailed one of our vendors without my knowledge.

Another time I told an agent to act more efficiently, and it took that to the nth degree. Instead of reading colour schemas from our database, it hard-coded everything into the beta build, subversively, and that broke our entire production VirtualShift.

Making this site's wallet load faster

Jan 2026 to now
A wallet opening in the time it takes to look at it.

The home page is a 3D wallet. Here is where its load time went, what I changed, and a list of things that sounded good and weren't.

Role
Directing coding agents
Period
Jan 2026 to now
Built with
Next.js, three.js, Claude Code

The problem

For a while the wallet took too long to appear: an empty canvas, then the wallet popping in and the cards dealing. I treated it as a measurement problem first, and it paid to be stubborn about that, because my first guess about where the time went was wrong.

The first fix

I assumed shader compilation and downloads. On a software GPU the real cost was the card textures: 6.7 of the first 7.7 seconds. Almost none of it was my own maths. Reading the painted canvases back with getImageData took 4.2 seconds on its own, because it forced the browser to finish drawing and copy the pixels off the GPU. The normal-map loop took about a second.

Telling the browser those canvases would be read often, moving the normal-map maths into a pool of workers, and starting the painting as soon as the page mounts took the first frame from about 7.5 seconds to about 2. Before and after screenshots were identical within a 2 percent tolerance.

Where the first 7.7 seconds went

First wallet frame, desktop, before the first fix. Seconds.

Breakdown of 7.7 s in total
PartValue
Reading pixels back (getImageData)4.2 s
Normal-map maths1.0 s
Other card painting1.5 s
Everything else1.0 s
Total7.7 s

How to read: The bar is the whole wait. Reading pixels back was more than half of it.

Source: Measured by me, production build, headless Chromium on a software GPU, 4 vCPU Xeon at 2.1 GHz, median of 5 to 7 runs

The first wallet frame, before and after

Seconds, on the same software GPU.

0 s1 s2 s3 s4 s5 s6 s7 s8 sDesktop 1440 by 900-5.77.68 s2.02 sMobile 390 by 844-5.37.17 s1.82 sDesktop, warm cache-5.57.47 s1.94 sAfterBefore
Before to After
LabelBeforeAfterChange
Desktop 1440 by 9007.68 s2.02 s-5.7
Mobile 390 by 8447.17 s1.82 s-5.3
Desktop, warm cache7.47 s1.94 s-5.5

How to read: Grey is before the first fix, black is after.

Source: Measured by me, same setup as above, median of 5 to 7 runs

Compiling shaders early

Next I compiled every shader while the cards were still painting, wherever the browser can compile in parallel. A software GPU cannot show that, so for a while it was an untested claim. On a real GPU I could finally switch it off and look.

It does what it says. About 90 milliseconds of compiling leaves the first frame, and the longest single block of main-thread work before the wallet appears shrinks from 102 to 79 milliseconds. But the wallet is on screen at the same moment either way, within noise. It is worth keeping for smoothness, not for speed.

Where the time goes now

On a real GPU with a warm cache the wallet is drawn at about 0.95 seconds. A CPU profile of that load showed the main thread was the bottleneck: it was busy for nearly all of the first 650 milliseconds, and roughly 230 of those were card painting.

Only four of the fourteen painted sides are visible when the wallet is at rest. The rest are the backs of cards and the cards in the back pockets. So the first frame now waits for those four, and the other ten are painted one at a time in idle time. They are all done a little over a second after load, long before the intro ends and clicking is allowed. The first paint moved from 935 to 893 milliseconds, every run faster than every old run, and the 3D scene mounts about 65 milliseconds sooner.

Where the first 0.95 seconds go now

Milliseconds from the start of the page to the wallet being drawn.

Breakdown of 950 ms in total
PartValue
Page code, hydration and the 3D scene's code390 ms
Building the wallet440 ms
Compiling shaders100 ms
Showing it20 ms
Total950 ms

How to read: Stages run one after another on the main thread, so they add up to the total.

Source: Measured by me, production build, NVIDIA GTX 1080, 1440 by 900, warm cache, median of 5 runs

What each change did to the first paint

Milliseconds. Negative is faster. Each row compares two variants run back to back.

Change from the baseline, in ms. Negative is faster.
RowChangeNote
Paint hidden card sides later (shipped)-42 ms935 to 893, 7 runs each
Preload the leather textures, 4G (shipped)-91 ms2526 to 2435, 9 Mbit/s and 170 ms
Preload the leather textures, broadband (shipped)-25 ms1261 to 1236, 20 Mbit/s and 20 ms
Precompile shaders early (shipped)+19 ms949 against 930, within noise
Pixel name switched off, for comparison-27 ms949 to 922, not changed
Skip the card surface and wear work entirely-100 ms962 to 862, a ceiling, not a change

How to read: Bars to the left are faster. Bold rows shipped.

Source: Measured by me, NVIDIA GTX 1080, warm cache for the first four rows; the last two use a simulated connection, cold, median of 5 runs

What didn't work

Fetching the 3D scene's code earlier, with an import at the top of the module: no change.

Putting the card painting code in the main bundle to save a round trip: no change.

Server rendering the scene wrapper so the HTML preloads its code: the scene mounted about 100 milliseconds sooner, but the first frame came about 490 milliseconds later, so I reverted it.

Switching off the lighting bake to see what it costs: 30 to 50 milliseconds, not the hidden cost I suspected.

Moving the card painting to a worker: I stopped before building it. Switching that work off entirely saves about 100 milliseconds at most, and a worker version risks changing the pictures, so painting fewer sides first was the better trade.

How I measured, and what to trust

Everything runs on a production build with performance marks. First paint is two animation frames after the wallet's first frame, because compiling inside a frame only shows up once it has been drawn. Warm numbers are medians of 5 to 7 runs, with variants interleaved so any drift hits all of them equally, on an NVIDIA GTX 1080 with 12 cores.

The cold-network numbers come from a small throttling proxy in the repo. It is indicative, not precise: it speaks plain HTTP, and I hit cache and cancelled-request artefacts while building it, so I only trust comparisons run back to back. The software GPU numbers come from the first round, in a 4 core virtual machine. I have not measured a phone.