Full-Stack Engineer · AI & Automation

I build systems that save real money.

I work across React/Next.js, Python, and .NET — and ship the hard parts: data pipelines, cloud, and real UX.

production apps
5+production appsshipped, and used every day
daily users
30+daily usersacross a 50-person engineering firm
React.NETPythonNode.jsSQLAgentic AI
Résumé
804hours
reclaimed
$130K · 2025–26

2025 · full year243 hrs
2026 · ytd · q2561 hrs
the budget automator — 71% of 2026, the rebuild that doubled its per-project yield
  • budget automator
  • delivery manager
  • plot automator
  • in-house file explorer
  • element linker
  • cutsheet manager
Once per project, on a constant flow of them — so it recurs every year. Modeled from the firm’s own ROI accounting at its average billable rate; not measured.
Hours reclaimed by the tools I built, by year, from the firm’s ROI accounting
PeriodHours
2025 (full year)243
2026 (ytd · q2)561
Total 2025–26804 hours ($130K)

Flagship · production ecosystem

The software platform behind a 50-person engineering firm

A fragile internal toolset, grown into the software platform a 50-person engineering firm runs its week on.

financial-analysis & trading bots
also built
across finance, AI & industrial automation

The firm wanted to use AI. Nobody asked what it would have to read. So I built that first — governed data, and documents a machine can process.

01.1The Product

A document-delivery desktop app, shipped as a product

I ship products, not demos. Somebody’s Monday depends on this thing booting.

A WinForms v1 grown into a WPF / .NET 9 rewrite on Clean Architecture + EF Core. A hybrid integration against a commercial ERP — its API for keys, ODBC for detail — pulls ~600 projects in ~2 seconds. 970 tests. An installer that bundles the runtime, a channel auto-updater, and a forced v1 → v2 migration, because the difference between an app and a product is what happens on the machine you will never see.

  • .NET 9
  • WPF
  • EF Core
  • Clean Architecture
  • xUnit
Deliverables, with due-date state, review hold, and ownership
DeliverableDueHoldOwner
Riser diagram · level 3
D-1042
Overdueyours
Load calculation · east wing
D-1043
Due soonyours
Panel schedule · rev C
D-1044
On trackHeld
Specification · fire alarm
D-1045
Due soon
Coordination report · MEP
D-1046
On track
01.2The Decision Engine

Workforce-capacity forecasting

I can model a business, not just build a UI. It pays for itself on a single avoided mis-hire.

A single-user desktop viewer rebuilt as a multi-user authenticated web engine: a Python / FastAPI service fusing a commercial ERP and CRM into three forecast models, shaping raw project demand into bell curves rather than flat monthly averages, against real staff capacity. ~90% less effort than the Excel process it replaced.

90%
effort cut
forecasting vs the old Excel process
  • Python
  • FastAPI
  • Next.js
  • OAuth2
  • Forecast modeling
JFMAMJJASONDcap
flat monthly average ──▶ shaped demand · staff capacity held constant
the honesty audit

Its first honest output was that the firm was 165% utilized. That number was false — the model was right and the inputs were not. I said so before anyone asked.

01.3The Method

Standards written to be read by a model

I build the factory, not just the car.

AI was the goal. This was the prerequisite. An agent reading a repo with no standards document infers the standards from the code — including the mistakes. So I wrote the documents: ~15 of them, ~8,600 lines, two stacks, a routing layer, six subagents. Then I watched a naming rule I had written, committed, and believed in get followed once in twelve tries. An authoritative document that never reaches the keystroke is decoration. What follows is what reaching the keystroke looks like.

every artifact in this repository carries one
2026-07-09NRA-103nrap-brain-analysisreadout
when
ISO, so it sorts
what
the ticket that asked
why
the change, in words
form
research, plan, or readout

The branch carries it. The commit carries it. The section you are reading carries it.You can walk any pixel on this site back to the ticket that asked for it.

000101.101.201.302030405

You have been reading a tagged document for the last four sections.

Also shipped at the firm
  • OCR doc-intelligence over 10,000+ files
  • 7,113-project search · sub-second
  • Company site — Next.js 15 / React 19 / Three.js / Supabase
  • Revit add-in suite · 4 versions (2023–2026)
  • 35+ zero-downtime releases
Deep dive · the method, enforcedHow the provenance actually holds — the governance model, and the mechanism that beats the rule.Read more

Governance, with edges

A standards system is only as real as the things it refuses. These are the edges — the parts a reader can check rather than take on faith.

Deliverable taxonomy
4 tiers, 22 valid combinations, full compatibility matrix
Access control
14 role codes, 4 access levels, most-restrictive default
Versioning
SemVer from 0.1.0; 4 version fields kept in sync by script
ERP integration
3-step session lifecycle; 11 methods documented as non-functional
Design tokens
8 named type tokens, 12 px minimum, 6 spacing values
Agent workflow
6 subagents, 8 commands, a 7-state ticket workflow

The compatibility matrix

64 combinations · 22 legal · derived, not listed

The type was a substring once — [FR], [HD], [PH], bracketed into the name, where nothing could validate it. I made it a foreign key and the modifiers typed columns. Then the matrix below became answerable.

Every combination of the deliverable type and its four modifiers. Select a cell to see whether it is legal, or every invariant that rejects it.
Deliverable type····h····p··hp····i·h·i··pi·hpi····rh··r·p·rhp·r··irh·ir·pirhpir
Issuance
Final Review
Constraint
Open Item

columns: h = hold p = placeholder i = intermediate-review r = requires-specs

Select a cell. The illegal ones are the interesting ones.
  • I1  An intermediate review is set only on a constraint.
  • I2  Placeholder content is set only on milestone types — an issuance or a final review.
  • I3  A placeholder is never on hold — there is nothing yet to hold.
  • I4  An open item never requires specifications.

Writing the matrix down is what makes “is this state legal?” a lookup instead of an argument. The legal set above is derived from those four rules — change one and the count changes with it, and the build fails until the page agrees.

The finding

Auditing my own document set, I found a live credential stored in plaintext in one of the reference docs — a service username and a password, committed to a network share the whole team could read, sitting next to the API endpoint it authenticated against. I flagged it for rotation and redaction.

That is a genuine failure, and it is mine. It is also exactly the failure the standards themselves warn against: the coding standard has a security section that mandates a secrets manager and states, in as many words, never commit secrets. The doc set knew better than the doc set.

A rule I wrote, believe in, and enforce on others did not stop me from pasting a password into a reference doc while debugging an API.Rules rely on attention. Mechanisms don’t.engineering-standards SOP system, §9 — what I’d improve
prose
a standard a person is asked to remember
agent context
a standard a model reads before it writes
executable rule
a standard that fails the commit

That last rung is the one this page stands on. Below is a rule from the same system, running as a mechanism — in your browser now, and on the commit that shipped this section.

The rule, and the thing that keeps it

thoughts/shared/{plans,research}/YYYY-MM-DD-<TICKET>-<slug>.md

filename
The linter decides. This page does not get a vote.

That convention was written down, committed, and correct. Across the twelve artifacts that existed before anything enforced it, it was followed once. The eleven that missed it are still here, grandfathered — a lint that blocks every commit on day one gets disabled on day two.

── pre-commit ──────────────────────────────
✓ lint-filenames: passed
✓ leak-scan: no secrets, no identifiers on shipping surfaces
✓ check-filename-rule: the rendered rule matches the enforced rule
────────────────────────────────────────────

This repository’s pre-commit hook, on the commit that shipped this section. Line two is the scanner the finding above should have had. Line three checks that the rule on this page is the same string line one enforces. Bypass is --no-verify — and if you find yourself typing it, the rule is wrong. Fix the rule, not the commit.

Signature project · quantitative systems

Trader — how I found out my strategy didn’t work

I built an event-driven forex bot on a practice OANDA account, then spent most of the project doing the unglamorous part: pulling eight years of candles down from the broker, pickling them, and testing whether the strategy actually made money. It mostly didn’t. Scroll the pipeline below — collect, store, compute, backtest, report, tune — and you’ll watch the thing that matters get built: not a money printer, but an instrument that tells me the truth about a strategy. Most strategies aren’t true. This one taught me to check.

PRACTICE ACCOUNT

Showing the bands the bot watches. The scroll-through pipeline runs on a wider screen. Candles are procedurally generated; the formulas are the bot’s real ones.

  1. Collect

    The broker hands you 3,000 candles per request, so you walk the window forward in time steps until you reach the end date. Three retries per window, because the API drops requests and an eight-year gap in your data is a bug you find months later.

    • Window: 2016-01-07 → 2023-12-31
    • Per request: 3,000 candles
    • Retries: 3
    infrastructure/collect_data.py
    CANDLE_COUNT = 3000
    INCREMENTS = { 'M5': 5 * CANDLE_COUNT,
                   'H1': 60 * CANDLE_COUNT,
                   'H4': 240 * CANDLE_COUNT }
    
    while to_date < end_date:
        to_date = from_date + dt.timedelta(minutes=time_step)
        candles = fetch_candles(pair, granularity, from_date, to_date, api)
  2. Store

    Paginated fetches overlap at the seams, so the same candle arrives twice. Drop duplicates on time, sort, reset the index, freeze it to disk. Pickles rather than CSVs — the dtypes survive, and reload is instant when you are about to read the same frame four hundred times.

    • Pickles: 65 files
    • Instruments: 23
    • Timeframes: M5 · H1 · H4
    infrastructure/collect_data.py
    final_df.drop_duplicates(subset=['time'], inplace=True)
    final_df.sort_values(by='time', inplace=True)
    final_df.reset_index(drop=True, inplace=True)
    final_df.to_pickle(f"{file_prefix}{pair}_{granularity}.pkl")
  3. Compute

    Every indicator is a rolling window over the typical price — no loops, no state. Bollinger bands are a moving average plus and minus a scaled standard deviation. The band is not a prediction; it is a statement about how unusual this price is relative to the last twelve.

    • Indicators: Bollinger · ATR · Keltner · RSI · MACD
    • Signal: close crosses band, open inside
    • Gates: spread ≤ max · gain ≥ min
    technicals/indicators.py
    typical_p = (df.mid_c + df.mid_h + df.mid_l) / 3
    stddev = typical_p.rolling(window=n).std()
    df['BB_MA'] = typical_p.rolling(window=n).mean()
    df['BB_UP'] = df['BB_MA'] + (stddev * s)
    df['BB_LW'] = df['BB_MA'] - (stddev * s)
  4. Backtest

    This is where the project actually happened. I swept the parameter grid and watched almost all of it lose money. The first backtester was too slow to iterate on, so I vectorized it; when that was still too slow I ran the pairs across processes. Speed is not vanity here — it is how many hypotheses you get to kill per evening.

    • Combos swept: 32
    • Profitable: 3
    • Breakeven win rate: 41.7% @ RR 1.4
    simulation/guru_tester.py
    # guru_tester.py  →  guru_tester_fast.py  →  ema_macd_mp.py
    def is_trade(row):
        if row.DELTA >= 0 and row.DELTA_PREV < 0:
            return BUY
        elif row.DELTA < 0 and row.DELTA_PREV >= 0:
            return SELL
        return NONE
  5. Report

    Results go to Excel, one sheet per pair with an embedded line chart, because a notebook cell is where you compute a result and a spreadsheet is where you sit and stare at it until you believe it. The file ma_sim_H1.xlsx is still in the repo. It is 419 KB of me being wrong.

    • Output: ma_sim_H1.xlsx · 419 KB
    • Per pair: trades · total · mean · min · max gain
    • Notebooks: 16 in exploration/
    simulation/ma_excel.py
    chart = book.add_chart({'type': 'line'})
    chart.add_series({
        'categories': [sheetname, start_row, labels_col, end_row, labels_col],
        'values':     [sheetname, start_row, data_col,   end_row, data_col],
    })
    chart.set_title({'name': title})
  6. Tune

    What survives the backtest becomes configuration. The live bot loads it at boot, polls for newly closed candles, and sizes every position so a stop-out costs exactly $20 — whatever the pair, the pip location, or the distance to the stop. And it refuses to open a second position on a pair it is already in, because it once stacked entries all the way down.

    • Risk per trade: $20, always
    • Poll: M1 · newly closed candles
    • Guard: trade_is_open()
    bot/settings.json
    { "trade_risk": 20,
      "pairs": { "EUR_USD": { "n_ma": 12, "n_std": 2.5,
                              "maxspread": 0.0004,
                              "mingain": 0.0014,
                              "riskreward": 1.4 } } }
    
    # trade_manager.py — refuse a second position on the same pair
    ot = trade_is_open(trade_decision.pair, api)
    if ot is not None: return None
  • Collect → pickle → pandas → backtest → tune
  • 8 years of candles · 23 instruments · 3 timeframes
  • Bollinger mean-reversion · risk-sized positions
  • Backtested 32 parameter combos — 3 made money
  • Python · pandas · MongoDB · Flask · React

This runs on an OANDA practice account — no real money has ever been at risk, and I show no P&L because there is none worth showing. The candles in the simulation are procedurally generated; the thresholds, formulas, risk math, and signal logic are the bot’s real ones, ported line-for-line. The backtest result is the honest one: at a 1.4 risk-reward ratio you must win 41.7% of trades to break even, the strategy wins about exactly that, and then the spread takes it negative. That finding is the project.

Read the architecture
  • A paginated collector walks the OANDA API 3,000 candles at a time from 2016-01-07 to 2023-12-31, retrying three times per window, then dedupes on timestamp, sorts, and writes one pickle per instrument-timeframe — 65 files across 23 instruments and M5/H1/H4.
  • Indicators are pure pandas over those frames: Bollinger bands on the typical price, plus ATR, Keltner channels, RSI, and MACD — each a rolling window, no loops.
  • The backtester grew up in public: a straightforward guru_tester.py, then a vectorized guru_tester_fast.py when it got too slow to iterate on, then ema_macd_mp.py when even that needed multiprocessing across pairs.
  • Results land in Excel via xlsxwriter — one sheet per pair with an embedded line chart — because a spreadsheet is what you actually stare at when you are deciding whether a strategy is real.
  • The winning parameters become bot/settings.json, which the live bot loads at boot. It polls for newly closed M1 candles, computes bands, checks spread and minimum gain, sizes the position so a stop-out costs exactly $20, and refuses to open a second position on a pair it is already in.

Signature project · agentic systems

jarvis — an agentic loop that hears, reasons, and acts

waking…
  1. Acquire“Hey Jarvis”
  2. UnderstandWhisper
  3. ReasonLLM
  4. DeliverStreaming TTS

Auto-playing the loop — the orb’s state lights the matching stage. Tap “Act” to see the tools.

The same loop, in production at the firm

  1. AcquireERP (ODBC) + CRM (REST)
  2. PipelineAccess → SQL Server
  3. Search7,113 projects · 10k OCR docs
  4. Infer3-model forecast ensemble
  5. ActWPF / .NET · 30+ daily users
  6. Dashboard12-month KPI · Power BI
  7. Analyze$130K · ~90% effort cut

I built jarvis as a local voice assistant to prove the full agentic loop end-to-end: it hears you, reasons with a local or cloud model, calls real tools to act, and answers in under a second. The voice is just the demo — point that same loop at a business and the “act” step becomes forecasting finances, an employee portal, inventory, or QC on CAD models. That’s the pattern I run in production at the firm: data from two business systems → a 3-model forecast → dashboards 30+ people use daily.

  • Acquire → reason → act → deliver
  • Multi-LLM routing — local Ollama or cloud
  • Tool-calling that takes real action
  • Same loop in production: 3-model forecast · 7,113-project search
  • $130K reclaimed in production

jarvis itself is a local, personal build — a working proof of the loop, not a business product. The production numbers here are real: they’re from the platform I built at the firm, which runs this same acquire → reason → act pattern. Business tools shown beyond what’s shipped are illustrative of where the pattern goes.

Read the source on GitHub
Read the architecture
  • One interface sits in front of multiple model backends — a local Ollama model or a cloud LLM — so the same agent loop runs offline or online.
  • Whisper transcribes speech, the model decides on tool calls, and those tools act on the machine: the agent does things, it doesn’t just chat.
  • Replies stream into text-to-speech sentence-by-sentence, so the first audio lands in under a second instead of after the whole response.
  • Two-phase barge-in lets you interrupt mid-sentence, and the entire stack stays within a 16 GB memory budget on a Mac Mini.
  • The same loop, in production at the firm: a commercial ERP (ODBC) and CRM (REST) feed a 3-model forecast that lands on Power BI dashboards 30+ people use to plan staffing.

Signature project · AI infrastructure

NRAP — real-time brain-activation inference, rendered live

20,484 vertices → 7 functional groups, grouped by cortical position

worker.aggregate()   in    (t, 20484) float32      20,484 values   81,936 B = 80.02 KiB/timestep        └── static vertex→region lookup, precomputed            mean · peak · σ  per region  out   59 regions x 3 stats          177 values     ↓ 115.7x   raw → gzip → postgres BYTEA     full fidelity, cold  roi → postgres rows             what the frontend reads
uploading…
Input: 60 s instrumental clip
20,484 vertices

Peak: Default mode — driven by overall RMS energy

  • Auditory
  • Language
  • Emotion
  • Visual
  • Motor
  • Cognitive
  • Default mode
  1. UploadAudio · video · text
  2. EnqueueFastAPI → Redis
  3. Aggregate20,484 verts → 59 regions
  4. RenderThree.js stipple brain

Scripted recreation of the request path, not a live inference — a real run takes 30–60s on a rented A10G. Tap “Infer” to see the backends. The band → region mapping is NRAP's own, ported unchanged; only the band values are a deterministic stand-in. Nothing is playing. The sweep and the scatter are depictions — the worker collapses all 20,484 vertices at once, by a static lookup, and nothing comes apart.

Upload a song, a video, or a paragraph, and NRAP predicts how a brain would respond to it — then renders that response as a 3D particle brain that lights up in real time. Under the visual is the part I actually care about: Meta’s TRIBEv2 foundation model running on a rented GPU that costs $0 when nobody is using it, behind an interface that lets me swap the whole inference engine without the frontend noticing.

  • GPU inference at ~$0.02/clip · scales to zero
  • 3 swappable backends behind one output contract
  • Meta TRIBEv2 on an on-demand Modal A10G
  • 20,484 vertices → 59 regions, aggregated at write time
  • FastAPI + Postgres + Redis queue + worker

The brain above is a scripted recreation of the request path, not a live inference: the region mapping, the band-to-region affinities and the 20,484 → 59 aggregation are NRAP’s real code paths, but the activation values driving it are a deterministic stand-in rather than recorded model output, and nothing runs on a GPU in your browser. A real run takes 30–60 seconds on a rented A10G. NRAP itself is a working MVP, not a product: I built it to learn production ML infrastructure end-to-end. The predictions come from Meta’s TRIBEv2 research model and are not a clinical claim about any individual brain. TRIBEv2’s weights are CC-BY-NC, which is why the commercial-clean `features` backend exists alongside it.

Read the source on GitHub
Read the architecture
  • Three inference backends — a mock generator, a librosa feature-mapper, and the real Meta TRIBEv2 model — all satisfy the identical output contract: a (timesteps, 20484) float32 array. Switching between them is one environment variable and zero frontend changes.
  • TRIBEv2 runs on a rented Modal A10G that spins up on demand and scales back to zero, so idle cost is $0 and a 60-second instrumental clip costs about two cents. Routing skips the expensive extractors when the input does not need them — no video model for an audio file, no language model for instrumental music.
  • The API never calls the GPU directly. It enqueues onto Redis and a separate worker consumes the queue, so a GPU crash degrades throughput instead of taking the service down, and the two scale independently.
  • Vertex-to-region mapping is static, so the worker aggregates 20,484 raw vertices down to 59 stored regions at write time. Reads stay fast; the raw arrays are still kept, gzipped, for full fidelity.
  • The frontend samples a GLB mesh into a particle field and drives it from the activation timeseries — a stipple brain that fires as the inference results stream in, without shipping a shader-heavy model to the browser.

Portfolio

Selected work

A through-line of real products — most of them thread AI into something people actually use.

DevOverflow — live Q&A with AI answersLive demo AI-integrated

DevOverflow — live Q&A with AI answers

A Stack-Overflow-style Q&A platform: post a question and an LLM auto-drafts an answer alongside community responses.

What's hard: Genuinely deployed and clickable — real auth, Next.js, live on Vercel.

Nike Landing PageLive demo

Nike Landing Page

A focused, responsive marketing page built as a front-end craft sample.

What's hard: Pure responsive-UI craft — framed as exactly that, nothing more.

ThinkWell / LunaTeam project · my piece: Luna AI-integrated

ThinkWell / Luna

Cross-platform iOS + Android wellness app with peer-to-peer messaging and activity tracking. I built Luna — a ChatGPT-powered companion — solo.

What's hard: Luna threads OpenAI through a C#/Xamarin MVVM app; the Google Places finder, P2P messaging, and cross-platform build are the parts that genuinely shipped.

  • csharp
  • xamarin
  • android-color
  • apple-tile
  • sqlite-icon
Private / no public demo
Trader — algorithmic OANDA botPractice project

Trader — algorithmic OANDA bot

Event-driven forex bot trading a practice OANDA account with Bollinger-band mean-reversion, risk-based position sizing, and a React dashboard.

What's hard: Risk-sized positions across an ~80-pair engine; the real stack is Python / pandas / OANDA, not the old tag-soup.

  • python-icon
  • re
  • git
Private / no public demo

Get in touch

Let's talk

Looking for a full-stack engineer who ships and measurably saves money? I'm open to work.