AI Product Engineer

Melody McClure

Full-stack engineer building AI-native products end to end — from agentic backends to the apps people actually use.

About

I design, build, and ship AI-native products end to end. My recent work centers on agentic systems — software that reasons over real data, uses tools, and makes useful decisions — wrapped in interfaces people trust. I work across the stack: React/Next.js and Capacitor on the front, Python/FastAPI and Node on the back, primarily using Anthropic tools. I care about products that earn their place in someone's day rather than just demo well.

A quote I like

Move fast and break things.

Mark Zuckerberg

Technical toolkit

  • Next.js
  • React
  • TypeScript
  • JavaScript
  • Python
  • FastAPI
  • Node.js
  • Tailwind CSS
  • Anthropic / Claude API
  • Agentic AI
  • PostgreSQL
  • Supabase
  • SQLite
  • GraphQL
  • Capacitor
  • Fly.io
  • Netlify
  • Git

Selected work

Things I've built

A mix of shipped products and active experiments. Where there's a live demo, it's linked.

MyLyfe

Live · personal

An agentic health “Life-OS.” It composes each day's plan, meals, and workouts grounded in your real pantry, health metrics, and real-time glucose — using Claude tool-loops that substitute, combine, and verify against live data rather than follow a static script.

  • React
  • Capacitor
  • FastAPI
  • Claude
  • SQLite
  • Fly.io

FrontierNews

A daily, Claude-curated digest of AI frontier-lab news, presented in a clean newspaper-style reader. An automated pipeline gathers, ranks, and writes up the day's most important developments each morning.

It's a subscribers-only reader — use the demo password to explore the full thing.

Demo password: readall2026

  • Next.js
  • Claude
  • Netlify
  • Serverless
View live demo

In progress

Building now

An agentic harness exploring the edges of hardware capability and locally-run, non-cloud LLMs — pushing on what's possible when the model lives on the machine instead of behind an API.

  • Agentic systems
  • Local LLMs
  • Systems

Experience

Where I've worked

Eurest (Compass Group)

Administrator

Not pure code, but deeply technical: built the operational systems behind the launch of three new business units — translating complex operational needs into functional systems before teams arrived on-site. Created automated reporting tools that gave leadership real-time visibility into KPIs, guest behavior, and revenue trends, and refined customer-facing systems from direct guest feedback, driving a 12% revenue increase.

  • Operational systems
  • Automated reporting
  • KPI dashboards
  • SAP
Scenario

Full-Stack Developer

Full-stack development for an app that helps people make financial decisions around the lifestyle they're working toward.

  • Next.js
  • Supabase
  • Tailwind CSS
Old School Anti-Ageism Clearinghouse

Freelance Content & Taxonomy

Manage published resources and apply search taxonomy thoughtfully so the catalog stays genuinely useful.

  • Content strategy
  • Information architecture
induco

Full-Stack Engineer

Pre-seed startup marrying Web2 and Web3 to give people ownership of — and payment for — their personal data. Since closed.

  • Web3
  • React
  • Data ownership

Origins

Where it started

Long before I was building agentic systems, I was stuck on one question: can you make a computer think on its own? My first attempt was a tic-tac-toe game in a coding bootcamp, with an opponent I named Joshua — a nod to WarGames. He just plays random moves, so you can usually beat him. That little game was the seed of so much that I do now — and it's been fun to revive it.

It's also a fair before/after. Below is the original code I wrote, next to a version I refactored with AI — the same skill, pointed at my own work: keep what was good (the win-check, the immutable updates), fix what wasn't (a stale-closure bug, board logic that leaned on the DOM). The game you're playing runs on the refactored version.

And here's the payoff of cleaning it up: switch Joshua to '26 · thinks ahead and he goes from random moves to an unbeatable opponent — try it, the best you can do is a draw. That brain is minimax, and because the refactor had already made the win-check a pure function, teaching him to think was about a 25-line add-on, not a rewrite. Clean code makes the next thing cheap.

Play Joshua

Pick a side — shall we play a game?

TicTacToe.jsx
Original · 2021
import React, { useState, useEffect } from 'react';
import './Tic-Tac-Toe.css';

const initialState = {
  message: '',
  playerSide: '',
  playerToken: '',
  computerSide: '',
  computerToken: '',
  gameOver: true,
  board: [
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ],
  winOptions: [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ],
};

export default function TicTacToe() {
  const [state, setState] = useState(initialState);

  useEffect(() => {
    const gameStatus = checkGameOver();
    if (gameStatus) {
      handleGameOver(gameStatus);
    }
    //eslint-disable-next-line
  }, [state.board])

  const playerChoice = (value) => {
    let computer = '';
    value === 'X' ? (computer = 'O') : (computer = 'X');
    setState({
      ...state,
      playerSide: value,
      computerSide: computer,
      gameOver: false,
      message: `You are ${value}'s, Joshua is ${computer}'s`,
    });
  };

  const checkGameOver = () => {
    for (const winOption of state.winOptions) {
      const [a, b, c] = winOption.map(
        (index) => state.board[Math.floor(index / 3)][index % 3]
      );
      if (a && a === b && a === c) {
        return a === state.playerSide ? 'player' : 'computer';
      }
    }
    if (state.board.every((row) => row.every((cell) => cell !== ''))) {
      return 'draw';
    }
    return null;
  };

  const playerMove = (e) => {
    const id = e.target.id;
    const newBoard = state.board.map((row) => [...row]);
    newBoard[Math.floor(id / 3)][id % 3] = state.playerSide;

    setState({
      ...state,
      board: newBoard,
      playerToken: id,
    });
    setTimeout(() => compTurn(newBoard), 1000);
  };

  const compTurn = (currentBoard) => {
    const newBoard = currentBoard.map((row) => [...row]);
    const emptyCells = [];
    newBoard.forEach((row, rowIndex) =>
      row.forEach((cell, colIndex) => {
        if (cell === '') {
          emptyCells.push({ rowIndex, colIndex });
        }
      })
    );
    if (emptyCells.length === 0) {
      return;
    }
    const compMove = Math.floor(Math.random() * emptyCells.length);
    newBoard[emptyCells[compMove].rowIndex][emptyCells[compMove].colIndex] =
      state.computerSide;

    setState({
      ...state,
      board: newBoard,
    });

    const gameStatus = checkGameOver();
    if (gameStatus) {
      handleGameOver(gameStatus);
    }
  };

  const handleGameOver = (gameStatus) => {
    if (gameStatus === 'player') {
      setState({
        ...initialState,
        message: 'Congratulations, you won!',
      });
    } else if (gameStatus === 'computer') {
      setState({
        ...initialState,
        message: 'Sorry, Joshua won. Better luck next time!',
      });
    } else if (gameStatus === 'draw') {
      setState({
        ...initialState,
        message: 'It\'s a draw! Play again?',
      });
    }
  };

  const handleResign = () => {
    setState({
      ...initialState,
      message: 'You have lost because you gave up. Choose a side to play again.',
    });
  };

  return (
    <>
      <div id="game">
      <h4 className='announcement'>{state.message}</h4>
        {!state.playerSide ?
          <div id='tic-tac-toe-board'>
            <div className='player-choice'>
              <h4>Choose a Side</h4>
              <div className="side-choice">
                <h2 onClick={() => playerChoice('X')}>X</h2>
                <h2 onClick={() => playerChoice('O')}>O</h2>
              </div>
            </div>
          </div> :
          <div id="tic-tac-toe-board">
            {state.board.flatMap((grid) => grid)
              .map((square, idx) => {
                return square === state.playerSide
                  ? <div className='player-square' id={idx} key={idx} >{square}</div>
                  : square === state.computerSide
                    ? <div className='computer-square' id={idx} key={idx} >{square}</div>
                    : <div className='square' id={idx} key={idx} onClick={playerMove}>{square}</div>
              })
            }
          </div>}
        <div className="actions">
          {!state.gameOver &&
            <button id="give-up" disabled={state.gameOver === true ? true : false} onClick={handleResign}>Resign</button>
          }
        </div>
      </div>
    </>
  );
}
TicTacToeGame.tsx
Refactored with AI · 2026
"use client";

import { useCallback, useEffect, useState } from "react";

// The eight winning lines, as flat board indices (0-8) — the same idea from
// the 2021 version, kept because it was the cleanest part of the original.
const LINES = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6],
];

type Mark = "X" | "O";
type Cell = Mark | null;
type Outcome = { winner: Mark; line: number[] } | "draw" | null;

// Pure: given a board, who (if anyone) has won. No component state, no DOM —
// one source of truth the rest of the component can trust.
function getOutcome(board: Cell[]): Outcome {
  for (const line of LINES) {
    const [a, b, c] = line;
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return { winner: board[a] as Mark, line };
    }
  }
  return board.every(Boolean) ? "draw" : null;
}

// 2026 Joshua: a real brain via minimax. The board is tiny, so a full unpruned
// search is instant — and because getOutcome() is already a pure function, this
// is a ~25-line add-on, not a rewrite. Depth weighting makes him prefer faster
// wins and slower losses. Played perfectly, tic-tac-toe is unbeatable: you can
// only force a draw.
function minimax(board: Cell[], turn: Mark, me: Mark, depth: number): number {
  const o = getOutcome(board);
  if (o === "draw") return 0;
  if (o) return o.winner === me ? 10 - depth : depth - 10;
  const scores = board
    .map((cell, i) => {
      if (cell) return null;
      const next = [...board];
      next[i] = turn;
      return minimax(next, turn === "X" ? "O" : "X", me, depth + 1);
    })
    .filter((s): s is number => s !== null);
  return turn === me ? Math.max(...scores) : Math.min(...scores);
}

function bestMove(board: Cell[], me: Mark): number {
  let best = -Infinity;
  let move = -1;
  board.forEach((cell, i) => {
    if (cell) return;
    const next = [...board];
    next[i] = me;
    const score = minimax(next, me === "X" ? "O" : "X", me, 1);
    if (score > best) {
      best = score;
      move = i;
    }
  });
  return move;
}

const EMPTY: Cell[] = Array(9).fill(null);

type Mode = "random" | "thinking";

export default function TicTacToeGame() {
  const [board, setBoard] = useState<Cell[]>(EMPTY);
  const [playerSide, setPlayerSide] = useState<Mark | null>(null);
  const [thinking, setThinking] = useState(false);
  const [mode, setMode] = useState<Mode>("random");

  const computerSide: Mark | null =
    playerSide === "X" ? "O" : playerSide === "O" ? "X" : null;

  const outcome = getOutcome(board);
  const gameOver = outcome !== null;

  // '21 Joshua plays a random open square (beatable). '26 Joshua runs minimax
  // (unbeatable). The only difference is how this one line picks `choice`.
  // (The name is a nod to WarGames: "Shall we play a game?")
  const joshuaMove = useCallback(
    (current: Cell[], mark: Mark) => {
      const open = current
        .map((cell, i) => (cell ? null : i))
        .filter((i): i is number => i !== null);
      if (open.length === 0 || getOutcome(current)) return;
      const choice =
        mode === "thinking"
          ? bestMove(current, mark)
          : open[Math.floor(Math.random() * open.length)];
      const next = [...current];
      next[choice] = mark;
      setBoard(next);
    },
    [mode],
  );

  // After the player moves, let Joshua respond once the board has settled.
  useEffect(() => {
    if (!playerSide || !computerSide || gameOver) return;
    const marksPlaced = board.filter(Boolean).length;
    const playerToMove = marksPlaced % 2 === (playerSide === "X" ? 0 : 1);
    if (playerToMove) return;
    setThinking(true);
    const t = setTimeout(() => {
      joshuaMove(board, computerSide);
      setThinking(false);
    }, 600);
    return () => clearTimeout(t);
  }, [board, playerSide, computerSide, gameOver, joshuaMove]);

  function choose(side: Mark) {
    setBoard(EMPTY);
    setPlayerSide(side);
    setThinking(false);
  }

  function play(i: number) {
    if (!playerSide || board[i] || gameOver || thinking) return;
    const next = [...board];
    next[i] = playerSide;
    setBoard(next);
  }

  function reset() {
    setBoard(EMPTY);
    setPlayerSide(null);
    setThinking(false);
  }

  function pickMode(m: Mode) {
    setMode(m);
    reset();
  }

  const status = !playerSide
    ? "Pick a side — shall we play a game?"
    : outcome === "draw"
      ? mode === "thinking"
        ? "A draw — the best you can do against '26 Joshua."
        : "A draw. Play again?"
      : outcome
        ? outcome.winner === playerSide
          ? "You beat Joshua! 🎉"
          : "Joshua got you that time."
        : thinking
          ? "Joshua is thinking…"
          : `You're ${playerSide} · Joshua is ${computerSide}`;

  return (
    <div className="rounded-2xl border border-border bg-surface/60 p-6">
      <p className="eyebrow mb-3">Play Joshua</p>

      <div className="mb-4 inline-flex rounded-lg border border-border bg-surface-2 p-0.5 text-xs">
        {(
          [
            ["random", "'21 · random"],
            ["thinking", "'26 · thinks ahead"],
          ] as const
        ).map(([m, lbl]) => (
          <button
            key={m}
            onClick={() => pickMode(m)}
            className={`rounded-md px-3 py-1.5 transition-colors ${
              mode === m
                ? "bg-accent/20 text-accent"
                : "text-muted hover:text-fg"
            }`}
          >
            {lbl}
          </button>
        ))}
      </div>

      <p className="mb-4 min-h-5 text-sm text-muted">{status}</p>

      {!playerSide ? (
        <div className="flex items-center gap-3">
          {(["X", "O"] as const).map((s) => (
            <button
              key={s}
              onClick={() => choose(s)}
              className="h-14 w-14 rounded-xl border border-border bg-surface-2 font-display text-2xl font-bold text-fg transition-colors hover:border-accent hover:text-accent"
            >
              {s}
            </button>
          ))}
        </div>
      ) : (
        <div className="grid w-fit grid-cols-3 gap-2">
          {board.map((cell, i) => {
            const winning = outcome && outcome !== "draw" && outcome.line.includes(i);
            return (
              <button
                key={i}
                onClick={() => play(i)}
                disabled={!!cell || gameOver || thinking}
                className={`flex h-16 w-16 items-center justify-center rounded-xl border font-display text-2xl font-bold transition-colors ${
                  winning
                    ? "border-accent bg-accent/15 text-accent"
                    : "border-border bg-surface-2 text-fg hover:enabled:border-accent/60"
                } ${cell === computerSide ? "text-accent-2" : ""}`}
              >
                {cell}
              </button>
            );
          })}
        </div>
      )}

      {playerSide && (
        <button
          onClick={reset}
          className="mt-5 text-sm text-muted underline-offset-4 transition-colors hover:text-accent hover:underline"
        >
          {gameOver ? "New game" : "Resign"}
        </button>
      )}
    </div>
  );
}

The Mars Rover search

Bootcamp origin

Another bootcamp seed. The assignment was BloomTech's “NASA Photo of the Day” — but I pointed it at NASA's Mars Rover catalog instead. It grew from a hard-coded Curiosity date, to a random-image loader, to a search across all four rovers by mission date — handling the fact that each rover carries different cameras and has different service windows by modeling state as one flexible object. It's where I started thinking about shaping data from a real API into a UI.

NASA has since retired that photo API, so it lives on as code rather than a live demo — recently kept building on a modern Vite stack.

  • React
  • NASA API
  • State as an object
  • Vite

Contact

Let's build something.

Have a project in mind or just want to talk shop? Send a note and I'll get back to you, usually within a day.

melody@melodymcclure.com