$ whoami
Math taught me to see problems as puzzles. Code taught me to solve them. Now, I’m teaching an AI to join in.
Shipped
Not a demo. A product real people download and run.
$ wispkin --version
0.1.0-beta.2 · public beta · Windows 10 / 11
Wispkin
An AI assistant with a face, living on your Windows desktop.
Chispa floats over your windows and reacts to what the AI is doing: thinking, working, waiting for your permission, done. Click it and you're chatting with Claude (your own API key, able to edit files and run commands only with your approval) or with a local Ollama model that never leaves your machine. No accounts, no telemetry, auto-updating installer.
- architecture decision records
- 12
- specialized AI subagents
- 8
- unit tests in CI
- 381
- QA checks on the real app
- 265
The source repo is private; the snippets below are condensed from it.
Built with Claude Code — directed, not delegated
I didn't ask an AI to “make an app”. I ran it like a team: I owned the architecture, the contracts and the final call on every change.
Every phase started with an ADR (12 in total: multi-provider brain, security model, spending cap…). The work was split across eight specialized subagents with narrow scopes and tools — the reviewer is read-only and audits credentials, IPC and permissions; QA drives the real Electron app, not just the test suite. 381 unit tests gate every push on Windows CI, and the beta only shipped after 259 of 265 real-app checks passed and the one remaining blocker was fixed.
Three decisions that made it shippable
One contract, any AI provider
Claude (via the Agent SDK) and Ollama implement the same BrainProvider interface and emit identical events. Each provider declares its capabilities, and the UI shows or hides features from those flags.
Why: Users without a paid key can run fully local and free, and adding an OpenAI-compatible backend is one new file — the avatar and chat don't change.
// Every AI backend implements the same contract and emits the same
// AgentEvents. The avatar, the chat and the state machine never know
// who is answering — adding a provider touches one file, not the UI.
export interface BrainProvider {
// Declares capabilities (tools? local? billed per use?) so the UI
// shows or hides features instead of branching on provider ids.
readonly info: ProviderInfo;
check(ctx: ProviderContext): Promise<CheckResult>;
listModels(ctx: ProviderContext): Promise<ModelList>;
openConversation(opts: ConversationOptions): Conversation;
}
export interface Conversation {
send(text: string): void;
// Every permission request MUST be answered: closing the bubble or
// interrupting resolves it as "deny". Nothing is ever left hanging.
answerPermission(requestId: string, decision: PermissionDecision): void;
interrupt(): Promise<void>;
onEvent(listener: (event: AgentEvent) => void): () => void;
}
/** Decrypted API key: lives only in the main process memory. */
export interface ProviderContext {
config: ProviderConfig;
secret: string | null; // never logged, never sent to the renderer
}
export function createProviders(): Record<ProviderId, BrainProvider> {
return {
"claude-agent": createClaudeAgentProvider(), // Agent SDK, user's own key
ollama: createOllamaProvider(), // local HTTP API: free, stays on-device
};
}Puzzle
Draw a wall. Watch it find a way around.
This is the “math taught me to see problems as puzzles” part, made interactive. Drag to draw walls, drag the A/B markers anywhere — A* re-solves it live.
How I think
Real decisions, and the reasoning behind each one.
Optimization under high traffic
A media platform's article pages went from a few hundred to tens of thousands of concurrent readers within minutes of a story going viral — and every one of them was hitting the same API endpoint at once.
When traffic spikes, the worst decision is letting every user fire their own call to the backend. Here's how I design the caching layer so it can absorb the spike without falling over.
Decision: Stale-while-revalidate caching for API calls, combined with deduplication of concurrent requests, so a traffic spike produces one backend call instead of thousands of identical ones.
Concurrent requests dedupe into one backend call; a later one gets instant stale data while it revalidates in the background.
Multi-repo / DevOps
A small engineering team was shipping to both a QA and a production environment, with deploys handled by hand — meaning the only thing standing between a broken branch and production was whoever remembered to run the checks that day.
This isn't feature code — it's process design. The job here wasn't writing a clever function, it was making sure the whole team could ship without anyone having to remember to be careful.
Decision: A CI/CD pipeline with quality gates (lint, test, build) that every branch must clear, where QA deploys automatically off develop and production only ships from main behind a required-reviewer environment.
Same gates for every branch — production is the only one that waits for a human.
Local AI agent with Ollama
A personal project: a local agent that runs entirely on-device against Ollama, and needs to act on the real world — check the weather, do a conversion, look something up — without ever giving the model direct access to the filesystem, network, or shell.
The model doesn't get to run anything. It gets to ask. This is the pattern that draws that line — and it's also the base for the playground below, simplified to run entirely in your browser.
Decision: Tool-calling orchestration: the model is given a fixed catalog of named tools, its response is parsed for a structured tool call, and only the orchestrator — never the model itself — executes it.
interface Tool {
name: string;
description: string;
execute: (args: Record<string, unknown>) => string;
}
const tools: Tool[] = [
{
name: "get_weather",
description: "Get the current weather for a city",
execute: ({ city }) => `${city}: 22°C, clear skies`,
},
{
name: "convert_currency",
description: "Convert an amount from one currency to another",
execute: ({ amount, from, to }) =>
`${amount} ${from} ≈ ${(Number(amount) * 1.08).toFixed(2)} ${to}`,
},
];
// The model never calls a function directly — it returns structured JSON
// naming a tool and its arguments. We parse that decision ourselves and
// decide whether to act on it, which is what keeps the model sandboxed.
interface ToolCall {
tool: string;
args: Record<string, unknown>;
}
function parseToolCall(modelOutput: string): ToolCall | null {
try {
const parsed = JSON.parse(modelOutput);
if (typeof parsed.tool === "string") return parsed as ToolCall;
} catch {
// Not valid JSON — treat the output as a plain text reply instead.
}
return null;
}
export async function runAgentTurn(modelOutput: string): Promise<string> {
const call = parseToolCall(modelOutput);
// No tool call parsed: the model just answered in plain text.
if (!call) return modelOutput;
const tool = tools.find((t) => t.name === call.tool);
if (!tool) return `Error: no tool named "${call.tool}"`;
return tool.execute(call.args);
}Trading systems (cTrader)
Two personal cTrader tools: a scalper exploiting a microstructure quirk in BTC/USD, and a signal indicator combining RSI with support/resistance.
Not "fair" trading — toxic flow. A real edge, exploited purely as a logic problem.
Decision: A two-tick confirmation window before betting on reversion, and an ATR-scaled confluence check between an RSI reversal and a fractal S/R zone.
One-tick jump beyond spread → two-tick confirmation → reversion trade back to the pre-jump level.
Try it yourself
The tool-calling pattern from the case study above, live.
Edit the code, change what the "model" replied with, and run it — everything executes right here in your browser, no server involved.
Stack
$ cat toolkit.json
// when I need iteration speed
// when the project grows
Also in the toolkit
Shipped production code with these too — not my default reach, but ready if a project needs them.
Frontend
JavaScript · HTML · CSS · Sass/Less/Scss · jQuery · Angular · Redux · GraphQL
Backend, CMS & data
PHP · WordPress · Wagtail · Python · Java · SQL
Cloud & DevOps
Git · GitHub · GitLab · Bitbucket · AWS · GCP · PM2 · Keycloak
Tooling & testing
Webpack · npm · Jest · Enzyme · React Testing Library · SEO
Verified, not claimed
The performance and accessibility numbers behind this site.
This isn't a badge I made up — it's a real Lighthouse audit against a production build, and you can re-run it yourself any time.
$ lighthouse https://www.mauriciorodriguez.dev/en --preset=desktop,mobile
Measured 2026-08-06