seedproject-web/api/cli/seed-projects.php
Carlos Arias 43ae1e2015 Baseline: full site build (brand, pages, DB, agent-editable)
Establishes the deploy baseline on main so the admin agent's publish/rollback
has a clean starting point. Everything built to date: sumi-e brand system,
homepage, projects (DB-driven case studies), resume, about, services +
website-design detail, contact form + DB, changelog, favicon + share card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DoFYZY9gkGPNDqZ7NuEa9a
2026-07-23 20:21:28 +00:00

373 lines
16 KiB
PHP

<?php
/**
* Seed / refresh cja_projects.
*
* Idempotent: upserts on slug, so it is safe to re-run after editing the
* content below. Existing rows keep their project_id, so anything pointing at
* them stays valid.
*
* php api/cli/seed-projects.php
*/
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../config.php';
$pdo = new PDO(
sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', DB_HOST, DB_NAME),
DB_USER,
DB_PASS,
[PDO::ATTR_ERRMODE => 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";