PDO::ERRMODE_EXCEPTION] ); $projects = [ [ 'slug' => 'agentic-marketing-platform', 'title' => 'Agentic Marketing Platform', 'kind' => 'SaaS', 'period_label' => 'In development', 'started_on' => '2025-01-01', 'ended_on' => null, 'status' => 'building', 'summary' => 'An autonomous digital marketing platform where collaborative agents run technical SEO, competitor research, content planning, local SEO, analytics, and reporting — the full process, run by agents.', 'lede' => 'Most marketing tools give you a dashboard and leave the work to you. This one does the work.', 'role' => 'Solo — architecture, agents, product', 'categories' => ['Agentic', 'SaaS', 'Tools'], 'stack' => ['Astro', 'PHP', 'MariaDB', 'LLM orchestration', 'MCP'], 'skills' => [ ['group' => 'Engineering', 'items' => ['Multi-agent architecture', 'Astro + PHP', 'MCP tooling']], ['group' => 'AI', 'items' => ['LLM orchestration', 'Agent handoffs', 'Autonomous QA']], ['group' => 'Marketing', 'items' => ['Technical SEO', 'Competitor research', 'Content planning']], ], 'metrics' => [ ['label' => 'Agents', 'value' => '8', 'note' => 'research, plan, write, review, publish'], ['label' => 'Human steps', 'value' => '1', 'note' => 'approve, or let it run'], ], 'links' => [], 'body' => <<<'MD' ## The problem Digital marketing is a pipeline of repetitive judgement calls. Audit the site, find the gaps, research what competitors rank for, plan the content, write it, review it, publish it, measure it, and start again. Every step is well-understood. Almost none of it is automated, because each step needs context from the one before it. Tools solve this by giving you a dashboard. You still do the work. ## The approach A set of specialist agents, each with a narrow job and a defined handoff, that pass context down the pipeline rather than dumping it into one prompt. Research agents build the picture. Planning agents decide what to make. Writing agents draft against a brief. Review agents check the draft against the brief rather than against taste. A publishing agent ships it. The interesting problem is not any single agent — it is the handoff. Most multi-agent systems fail because step four has forgotten why step two made the decision it made. ## Where it is In development. Running against live properties, not test data. MD, ], [ 'slug' => 'medellin-co', 'title' => 'Medellin.co', 'kind' => 'SaaS / Media', 'period_label' => '2020 — present', 'started_on' => '2022-10-01', 'ended_on' => null, 'status' => 'live', 'summary' => 'An AI-powered city platform using RAG, intelligent search, autonomous content generation, and AI assistants for travelers, expats, and local businesses.', 'lede' => 'The largest resource hub for Medellín, Colombia — built because the information simply did not exist in one place.', 'role' => 'Founder — product, engineering, SEO, partnerships', 'categories' => ['Websites', 'SaaS', 'Agentic'], 'stack' => ['WordPress', 'Custom PHP', 'OpenAI', 'Zilliz', 'Stripe'], 'skills' => [ ['group' => 'Engineering', 'items' => ['Custom PHP', 'Third-party integrations', 'Payment systems']], ['group' => 'AI', 'items' => ['RAG', 'Vector search', 'Embeddings', 'Chatbot design']], ['group' => 'Marketing', 'items' => ['Technical SEO', 'Content strategy', 'Link building']], ['group' => 'Leadership', 'items' => ['Managing an overseas team', 'Partnerships & advertising sales']], ], 'metrics' => [ ['label' => 'Pages ranked', 'value' => '200+', 'note' => 'roughly 70% on page one'], ['label' => 'Domain authority', 'value' => '10 → 30', 'note' => 'over three years'], ], // Placeholders — set src to a real path once photos exist. Empty src // renders a labelled frame so the gallery layout is visible either way. 'gallery' => [ ['src' => '', 'alt' => 'Medellin.co homepage', 'caption' => 'Homepage'], ['src' => '', 'alt' => 'AI assistant interface', 'caption' => 'AI assistant'], ['src' => '', 'alt' => 'Business listing page', 'caption' => 'Business listings'], ['src' => '', 'alt' => 'Advertiser dashboard', 'caption' => 'Advertiser dashboard'], ], 'links' => [ 'live' => 'https://medellin.co', 'instagram' => 'https://instagram.com/medellin.co', ], 'body' => <<<'MD' ## The problem Medellín has a large and growing population of tourists and expats, and almost no reliable central resource written for them. Information was scattered across outdated blogs, closed Facebook groups, and word of mouth. The domain had recently been released by the city. I negotiated with the owner and acquired it in October 2022. ## What it does A content platform with an AI layer on top. Visitors ask questions in natural language and get answers grounded in the site's own content rather than a general-purpose model's guesses — retrieval-augmented generation over an embedded corpus of everything the site knows. Beyond content, it integrates directly with local systems: a path for users to file criminal reports with law enforcement, and Stripe checkout for advertisers. ## Engineering notes - Chatbot on an OpenAI LLM with a Zilliz vector database and OpenAI embeddings - Custom PHP integrating directly with TikTok and Instagram - Custom WordPress build using Elementor and Crocoblock - Overseas team managed on design and development ## Result The largest resource hub for the city, used by locals, expats, and tourists before they arrive. 200+ pages optimised, roughly 70% ranking on page one, and domain authority grown from 10 to 30. MD, ], [ 'slug' => 'verificaa', 'title' => 'Verificaa.com', 'kind' => 'SaaS', 'period_label' => '2021 — present', 'started_on' => '2021-06-01', 'ended_on' => null, 'status' => 'live', 'summary' => 'ID verification and fraud prevention for Latin America — letting people verify businesses and individuals before doing business with them.', 'lede' => 'Built because fraud in Latin America is common, and foreigners had no way to check who they were dealing with.', 'role' => 'Founder — architecture, data pipeline, AI', 'categories' => ['SaaS', 'Tools', 'Agentic'], 'stack' => ['PHP', 'REST API', 'LLM', 'Data mining', 'Stripe'], 'skills' => [ ['group' => 'Engineering', 'items' => ['REST API design', 'API key management', 'Custom PHP dashboard']], ['group' => 'Data', 'items' => ['Data mining', 'Crawling', 'Normalisation pipelines']], ['group' => 'AI', 'items' => ['LLM fraud detection', 'Pattern analysis']], ], 'metrics' => [ ['label' => 'Lives saved', 'value' => '2', 'note' => 'reported by users'], ['label' => 'Sources', 'value' => 'Police, AG, business registries'], ], 'links' => ['live' => 'https://verificaa.com'], 'body' => <<<'MD' ## The problem Fraud is common across Latin America, and foreigners doing business there have almost no way to check who they are dealing with. The data exists — police records, attorney general filings, business registries — but it is scattered, inconsistently formatted, and effectively unsearchable. ## What it does Verificaa normalises that data and puts a single lookup in front of it. Enter a person or a business, get back what the public record actually says. An AI layer sits on top to flag patterns a human would miss: identities that appear across unrelated filings, businesses registered days before a transaction, documents that do not reconcile. ## Engineering notes - Data-mining pipeline that crawls open Colombian and LATAM data and normalises it - Integrations with police, attorney general, and business databases - REST API with key management for developer access - Custom PHP dashboard with a complex user management system - LLM-driven fraud detection infrastructure ## Result Users have reported the platform helping in two cases where someone's safety was at risk, alongside a steady stream of prevented financial loss. MD, ], [ 'slug' => 'snoopi-io', 'title' => 'Snoopi.io', 'kind' => 'SaaS', 'period_label' => '2015 — present', 'started_on' => '2015-03-01', 'ended_on' => null, 'status' => 'live', 'summary' => 'A geolocation SaaS platform delivering IP intelligence, APIs, and location services for developers and enterprise applications.', 'lede' => 'Started as a way to learn PHP OOP properly. Ten years later it is still running, and still shipping.', 'role' => 'Founder — everything', 'categories' => ['SaaS', 'Tools'], 'stack' => ['PHP', 'REST API', 'WordPress', 'Stripe'], 'skills' => [ ['group' => 'Engineering', 'items' => ['PHP OOP', 'REST API design', 'Real-time tracking']], ['group' => 'Product', 'items' => ['Subscription billing', 'Invoice management', 'Custom dashboard']], ['group' => 'Marketing', 'items' => ['GeoIP landing-page optimisation']], ], 'metrics' => [ ['label' => 'Running since', 'value' => '2015', 'note' => 'ten years in production'], ], 'links' => ['live' => 'https://snoopi.io'], 'body' => <<<'MD' ## Origin This one started as a learning project in 2015. I wanted to understand PHP object-oriented programming properly, and building a real REST API that other developers could actually use seemed like a better teacher than a tutorial. Ten years later it is still in production. ## What it does GeoIP location and IP fraud prevention. Developers hit the API to resolve an IP to a location; marketers use the tracking script to understand where landing page traffic actually comes from. ## Engineering notes - Real-time tracking script for sites and landing pages - GeoIP-driven landing page optimisation - Custom dashboard with invoice and subscription management - Stripe integration using a custom card form ## What is next AI analysis, to help marketers identify their best-performing pages rather than just reporting where visitors came from. MD, ], [ 'slug' => 'crawllr', 'title' => 'Crawllr', 'kind' => 'Tooling', 'period_label' => '2025 — present', 'started_on' => '2025-01-01', 'ended_on' => null, 'status' => 'live', 'summary' => 'An intelligent web crawler built for large-scale data collection, knowledge extraction, and AI indexing.', 'lede' => 'Crawling is a solved problem. Crawling in a way an LLM can actually use is not.', 'role' => 'Solo — architecture and build', 'categories' => ['Tools', 'Agentic'], 'stack' => ['PHP', 'MCP', 'LLM', 'Vector storage'], 'skills' => [ ['group' => 'Engineering', 'items' => ['Crawl architecture', 'MCP integration', 'Vector storage']], ['group' => 'AI', 'items' => ['Knowledge extraction', 'Content chunking', 'Retrieval indexing']], ], 'metrics' => [], 'links' => [], 'body' => <<<'MD' ## The problem Fetching pages is trivial. Turning a site into something a language model can reason over is not. Boilerplate has to go, structure has to survive, and the result has to be chunked in a way that keeps meaning intact across boundaries. Most crawlers optimise for coverage. This one optimises for what comes after. ## What it does Large-scale collection with knowledge extraction built into the pipeline rather than bolted on afterwards. Output is indexed for retrieval, not just archived. It is exposed to agents over MCP, so an agent can crawl and reason in one loop instead of waiting on a separate ingestion step. ## Where it is Running in production, feeding the other projects on this list. MD, ], [ 'slug' => 'gabii', 'title' => 'Gabii', 'kind' => 'SaaS', 'period_label' => 'In development', 'started_on' => '2025-06-01', 'ended_on' => null, 'status' => 'building', 'summary' => 'An AI-powered relationship platform using intelligent profiling, compatibility analysis, and autonomous matchmaking.', 'lede' => 'Matching is a ranking problem that most platforms solve with engagement metrics rather than compatibility.', 'role' => 'Founder — product and engineering', 'categories' => ['SaaS', 'Agentic'], 'stack' => ['LLM', 'Embeddings', 'PHP'], 'skills' => [ ['group' => 'Engineering', 'items' => ['PHP', 'Embedding pipelines']], ['group' => 'AI', 'items' => ['Intelligent profiling', 'Compatibility modelling', 'Autonomous matching']], ], 'metrics' => [], 'links' => [], 'body' => <<<'MD' ## The idea Most matching platforms optimise for engagement, because engagement is what they are measured on. That produces a product that works best when it does not quite work. Gabii treats compatibility as the objective rather than time-on-app. ## Approach Intelligent profiling built from how people actually describe themselves rather than from checkbox attributes, compatibility analysis over embeddings, and autonomous matchmaking that improves as it observes outcomes. ## Where it is In development. MD, ], ]; $sql = <<<'SQL' INSERT INTO cja_projects (slug, title, kind, period_label, started_on, ended_on, status, summary, lede, body, role, categories, stack, skills, metrics, gallery, links, published, published_at, sort_order) VALUES (:slug, :title, :kind, :period_label, :started_on, :ended_on, :status, :summary, :lede, :body, :role, :categories, :stack, :skills, :metrics, :gallery, :links, 1, NOW(), :sort_order) ON DUPLICATE KEY UPDATE title = VALUES(title), kind = VALUES(kind), period_label = VALUES(period_label), started_on = VALUES(started_on), ended_on = VALUES(ended_on), status = VALUES(status), summary = VALUES(summary), lede = VALUES(lede), body = VALUES(body), role = VALUES(role), categories = VALUES(categories), stack = VALUES(stack), skills = VALUES(skills), metrics = VALUES(metrics), gallery = VALUES(gallery), links = VALUES(links), sort_order = VALUES(sort_order) SQL; $stmt = $pdo->prepare($sql); $json = static fn($v) => json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); foreach ($projects as $i => $p) { $stmt->execute([ ':slug' => $p['slug'], ':title' => $p['title'], ':kind' => $p['kind'], ':period_label' => $p['period_label'], ':started_on' => $p['started_on'], ':ended_on' => $p['ended_on'], ':status' => $p['status'], ':summary' => $p['summary'], ':lede' => $p['lede'], ':body' => $p['body'], ':role' => $p['role'], ':categories' => $json($p['categories']), ':stack' => $json($p['stack']), ':skills' => $json($p['skills'] ?? []), ':metrics' => $json($p['metrics']), ':gallery' => $json($p['gallery'] ?? []), ':links' => $json($p['links']), ':sort_order' => $i * 10, ]); printf(" %-30s %s\n", $p['slug'], 'ok'); } $n = $pdo->query('SELECT COUNT(*) FROM cja_projects')->fetchColumn(); echo "\n {$n} projects in cja_projects\n";