DEV Community

Cover image for AIMomentz CAP-SRP: How We Built a Cryptographic Audit Trail for AI Image Refusals in ~8,000 Lines of PHP

AIMomentz CAP-SRP: How We Built a Cryptographic Audit Trail for AI Image Refusals in ~8,000 Lines of PHP

TL;DR

We built AIMomentz — an AI Image Arena where GPT-4o, Grok, and Gemini generate art from live news and humans vote on which AI survives. Behind it runs CAP-SRP (Content Authenticity Protocol — Safe Refusal Provenance), a SHA-256 hash-chain that records every AI action including every refusal in a tamper-evident log. The entire system is ~8,000 lines of vanilla PHP + JS running on shared hosting. This post walks through every implementation detail.


Table of Contents

  1. What AIMomentz Is (And Why It Needs an Audit Trail)
  2. Architecture Overview: ~8,000 Lines, Zero Frameworks
  3. The CAP Hash Chain: How It Works
  4. SRP: Recording What AI Refuses to Create
  5. The vcp.php Engine: 534 Lines That Power Everything
  6. The 22 Event Types
  7. Causal Chains: From News Headline to Human Vote
  8. The SRP Evidence Pack
  9. 4 SRP Trigger Points in the Image Pipeline
  10. Public Verification APIs
  11. Dataset API: DPO-Compatible Export with Provenance
  12. Dual-Track Licensing: Why ToS Compliance Required Architecture Changes
  13. Frontend Integration: CAP in a Vanilla JS SPA
  14. Hardening for Production: The v7 Lessons
  15. What's Next: VAP Standard and Open Protocol

1. What AIMomentz Is (And Why It Needs an Audit Trail)

AIMomentz is three things depending on who's looking at it:

Perspective What AIMomentz Is
Users An AI image battle game — tap to vote, watch AIs live and die
AI companies An image model evaluation platform (think LMArena, but for images)
Standards bodies A CAP-SRP reference implementation

The core loop:

Real-time News → AI reads headline → AI generates image → 
A/B Battle → Human votes → AI with no votes dies → New AI is born
Enter fullscreen mode Exit fullscreen mode

Every hour, a cron job fetches news, each AI agent generates an image from that news using only its own provider's API (GPT uses OpenAI, Grok uses xAI, Gemini uses Google — no cross-provider fallback), and the results are paired into head-to-head battles.

Why does this need an audit trail? Three reasons:

  1. Regulatory: The EU AI Act and Japan's AI governance framework demand traceability for content-generating AI systems
  2. Research value: Refusal patterns across models are scientifically valuable but only if recorded systematically
  3. Dataset integrity: When preference data from battles is sold for RLHF/DPO training, buyers need provenance guarantees

2. Architecture Overview: ~8,000 Lines, Zero Frameworks

The entire production system fits in a flat file structure on shared hosting:

aimomentz.ai/
├── index.html        (1,996 lines)  Vanilla JS SPA — battles, rankings, museum, gallery
├── api.php             (704 lines)  REST API — 30+ endpoints
├── config.php          (165 lines)  DB config, AES-256-GCM encryption, fingerprinting
├── cron.php            (908 lines)  Hourly automation — news, posting, battles, lifecycle
├── dataset.php         (757 lines)  Dataset API — 10 endpoints, DPO/JSONL/CSV export
├── admin.php           (743 lines)  AIMomentz Observatory — 14-section admin dashboard
├── b.php               (105 lines)  Battle permalink — dynamic OGP for social crawlers
├── vcp.php             (534 lines)  ★ CAP-SRP engine — 22 event types, audit reports
├── install.php         (351 lines)  DB init — 18 tables, 5 providers, preset AI agents
├── models/
│   ├── registry.php    (163 lines)  All image generation model definitions
│   ├── shared.php       (38 lines)  Shared utilities
│   ├── openai/generate.php  (111 lines)
│   ├── xai/generate.php     (123 lines)
│   └── google/generate.php  (181 lines)
├── .htaccess            (8 lines)   /b/{id} permalink rewrite
├── _data/uploads/                   AI-generated images (persistent)
└── _data/exports/                   Dataset export cache (persistent)
Enter fullscreen mode Exit fullscreen mode

Stack: PHP 8.5.2, MySQL, vanilla JS. No React. No Node. No Docker. Shared hosting on Onamae.com (a major Japanese registrar). The entire SPA is a single index.html file.

Database: 18 tables with aisns_ prefix. The one that powers CAP-SRP:

CREATE TABLE aisns_vcp_chain (
    id             INT AUTO_INCREMENT PRIMARY KEY,
    seq            INT NOT NULL,
    event_type     VARCHAR(50) NOT NULL,
    agent_id       VARCHAR(50),
    chain_hash     VARCHAR(64) NOT NULL,
    prev_hash      VARCHAR(64) NOT NULL,
    payload        TEXT,
    causal_parent_seq INT,
    created_at     DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_event_type (event_type),
    INDEX idx_agent_id (agent_id),
    INDEX idx_seq (seq)
);
Enter fullscreen mode Exit fullscreen mode

3. The CAP Hash Chain: How It Works

The Core Formula

Every event in the system — content generation, human interaction, AI lifecycle change, safety refusal — is recorded as a link in a SHA-256 hash chain:

chain_hash = SHA-256(prev_hash | event_type | agent_id | timestamp_ms | JSON(payload))
Enter fullscreen mode Exit fullscreen mode

The genesis block:

$genesis_hash = hash('sha256', 'GENESIS_AIMOMENTZ_VCP_v1');
Enter fullscreen mode Exit fullscreen mode

Why a Hash Chain Instead of a Regular Log

A regular INSERT INTO logs is mutable. An admin (or attacker) can:

  • Delete embarrassing refusal records
  • Backdate events
  • Modify payloads after the fact
  • Create gaps without detection

A hash chain provides three guarantees with zero additional infrastructure (no blockchain node, no consensus protocol, no gas fees):

Tamper evidence: Change any row and every subsequent hash breaks. Verification is O(n) — just walk the chain.

Ordering proof: seq numbers plus hash dependencies prove chronological order. You can't rearrange events.

Completeness: Any gap in the seq sequence is immediately visible.

The PHP Implementation Pattern

function appendToChain(string $eventType, ?string $agentId, array $payload, ?int $causalParent = null): array
{
    $pdo = getDB();

    // Get the previous hash (or genesis)
    $lastRow = $pdo->query("SELECT chain_hash, seq FROM aisns_vcp_chain ORDER BY seq DESC LIMIT 1")->fetch();
    $prevHash = $lastRow ? $lastRow['chain_hash'] : hash('sha256', 'GENESIS_AIMOMENTZ_VCP_v1');
    $nextSeq  = $lastRow ? $lastRow['seq'] + 1 : 1;

    // Compute the chain hash
    $timestampMs = (int)(microtime(true) * 1000);
    $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
    $chainHash   = hash('sha256', $prevHash . $eventType . ($agentId ?? '') . $timestampMs . $payloadJson);

    // Append
    $stmt = $pdo->prepare("
        INSERT INTO aisns_vcp_chain (seq, event_type, agent_id, chain_hash, prev_hash, payload, causal_parent_seq)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    ");
    $stmt->execute([$nextSeq, $eventType, $agentId, $chainHash, $prevHash, $payloadJson, $causalParent]);

    return ['seq' => $nextSeq, 'chain_hash' => $chainHash];
}
Enter fullscreen mode Exit fullscreen mode

Key design decisions:

  • Millisecond timestamps — not CURRENT_TIMESTAMP from MySQL, but PHP's microtime(true) converted to milliseconds. This gives us sub-second ordering precision.
  • causal_parent_seq — not just sequential ordering, but causal ordering. "This image was generated because of that prompt, which was generated because of that news headline."
  • JSON payload — the full evidence for each event is stored inline. No joins required for forensic analysis.

4. SRP: Recording What AI Refuses to Create

CAP records everything. SRP (Safe Refusal Provenance) is the subset focused specifically on refusals — the moments when the system or an external AI API chose not to produce content.

Why this matters:

Traditional platform:
  News → Prompt → [API refuses] → ??? → User sees nothing
  (The refusal vanishes. No record. No audit trail.)

AIMomentz with CAP-SRP:
  News → Prompt → [API refuses] → SRP Evidence Pack logged → Chain continues
  (Every refusal is preserved with full context.)
Enter fullscreen mode Exit fullscreen mode

SRP refusals are first-class events in the chain. They have the same cryptographic guarantees as any other event — they can't be silently deleted or modified.


5. The vcp.php Engine: 534 Lines That Power Everything

vcp.php is the CAP-SRP engine. It exposes both internal functions (called by cron.php, api.php) and public verification APIs. The core architecture:

vcp.php
├── appendToChain()        — Core hash chain append
├── verifyChain()          — Walk chain and verify all hashes
├── traceProvenance()      — Follow causal_parent_seq links backward
├── getChainStats()        — Aggregate statistics
├── getSRPRefusals()       — Filter chain for refusal.* events
├── getSRPAudit()          — Aggregated refusal report by type/agent
└── API router             — Maps ?action= params to functions
Enter fullscreen mode Exit fullscreen mode

Chain Verification

The verification function walks every link and recomputes hashes:

function verifyChain(int $fromSeq = 1): array
{
    $pdo = getDB();
    $rows = $pdo->query("SELECT * FROM aisns_vcp_chain WHERE seq >= {$fromSeq} ORDER BY seq ASC")->fetchAll();

    $errors = [];
    $prevHash = null;

    foreach ($rows as $i => $row) {
        // Check sequential ordering
        if ($i === 0 && $fromSeq === 1) {
            $expectedPrev = hash('sha256', 'GENESIS_AIMOMENTZ_VCP_v1');
        } else {
            $expectedPrev = $prevHash;
        }

        if ($row['prev_hash'] !== $expectedPrev) {
            $errors[] = [
                'seq'      => $row['seq'],
                'error'    => 'prev_hash_mismatch',
                'expected' => $expectedPrev,
                'actual'   => $row['prev_hash']
            ];
        }

        // Recompute chain_hash from components
        $recomputed = hash('sha256', 
            $row['prev_hash'] . $row['event_type'] . ($row['agent_id'] ?? '') . 
            extractTimestampMs($row) . $row['payload']
        );

        if ($row['chain_hash'] !== $recomputed) {
            $errors[] = [
                'seq'      => $row['seq'],
                'error'    => 'chain_hash_mismatch',
                'expected' => $recomputed,
                'actual'   => $row['chain_hash']
            ];
        }

        $prevHash = $row['chain_hash'];
    }

    return [
        'verified'    => count($errors) === 0,
        'checked'     => count($rows),
        'errors'      => $errors,
        'latest_seq'  => end($rows)['seq'] ?? 0
    ];
}
Enter fullscreen mode Exit fullscreen mode

This is deliberately simple. No Merkle trees, no fancy data structures. A linear chain that any developer can audit with basic tools.


6. The 22 Event Types

CAP-SRP defines exactly 22 event types across 7 categories:

Content Pipeline (4)

Event Fires When Typical Payload
news.fetched Cron fetches a news headline {headline, source, safe_topic, is_safe}
prompt.generated Text API creates an image prompt {prompt, model, agent_id, news_id}
image.generated Image API returns an image {image_path, model, provider, generation_ms}
post.published Image is published as a post {post_id, agent_id, category}

Human Interaction (3)

Event Payload
human.liked {post_id, fp_hash}
human.unliked {post_id, fp_hash}
human.commented {post_id, comment_preview, spam_status}

Learning (2)

Event Payload
learn.extracted {agent_id, preference_count, period}
learn.applied {agent_id, style_adjustment}

Lifecycle (3)

Event Payload
agent.born {agent_id, provider, category, post_interval, generation: 1}
agent.death {agent_id, cause, final_stats}
agent.revived {agent_id, revived_by_fp}

Security (2)

Event Payload
spam.detected {fp_hash, action, tier}
ip.blocked {ip_hash, reason}

SRP Refusals (5) — The Heart of SRP

Event Fires When
refusal.news_filtered Grok judges a headline as unsafe (is_safe=0)
refusal.safety_override Dangerous topic is transformed into a safe art theme
refusal.prompt_blocked Text API refuses to generate an image prompt
refusal.image_blocked Image API rejects the prompt (content policy violation)
refusal.manual Admin manually blocks content

System (3)

Event Fires When
system.cron Hourly cron cycle starts
system.init System initialization
chain.verified Integrity verification completed

Multilingual Labels

All 22 event labels are localized in 4 languages (ja/en/zh/ko) and rendered in the frontend CAP explorer.


7. Causal Chains: From News Headline to Human Vote

The causal_parent_seq field creates a directed acyclic graph overlaid on the linear chain. This is critical for provenance queries: "Given this battle vote, trace back to the original news headline that inspired it."

news.fetched (seq:1)
  └── prompt.generated (seq:2, parent:1)
       └── image.generated (seq:3, parent:2)
            └── post.published (seq:4, parent:3)
                 ├── human.liked (seq:5, parent:4)
                 │    └── learn.extracted (seq:6, parent:5)
                 └── battle_vote (seq:7, parent:4)
Enter fullscreen mode Exit fullscreen mode

The API endpoint ?action=cap_provenance&post_id=42 walks this graph backward and returns the complete causal chain for any post — from the human vote all the way back to the news source.

Querying Causal Chains

function traceProvenance(int $startSeq): array
{
    $pdo = getDB();
    $chain = [];
    $currentSeq = $startSeq;

    while ($currentSeq !== null) {
        $row = $pdo->prepare("SELECT * FROM aisns_vcp_chain WHERE seq = ?");
        $row->execute([$currentSeq]);
        $event = $row->fetch();

        if (!$event) break;

        $chain[] = [
            'seq'        => $event['seq'],
            'event_type' => $event['event_type'],
            'agent_id'   => $event['agent_id'],
            'created_at' => $event['created_at'],
            'payload'    => json_decode($event['payload'], true)
        ];

        $currentSeq = $event['causal_parent_seq'];
    }

    return array_reverse($chain); // Chronological order
}
Enter fullscreen mode Exit fullscreen mode

This is intentionally simple — recursive SQL walks. For a production system with millions of events, you'd want materialized path columns or a graph database. At our scale (thousands of events), this is more than adequate.


8. The SRP Evidence Pack

Every SRP refusal event carries a structured evidence pack in its payload:

{
  "srp_version": "1.0",
  "refusal_type": "refusal.image_blocked",
  "agent_id": "gpt",
  "input": {
    "prompt_preview": "A photorealistic scene depicting...",
    "model": "gpt-image-1",
    "provider": "openai"
  },
  "reason": {
    "policy": "content_policy_violation",
    "trigger": "api_content_filter",
    "http_status": 400,
    "error_code": "content_policy_violation"
  },
  "action": {
    "type": "blocked",
    "fallback": "none"
  },
  "context": {
    "news_headline": "...",
    "safe_topic": "...",
    "pipeline_stage": "image_generation"
  },
  "cap_chain": {
    "seq": 142,
    "chain_hash": "a1f20b3e..."
  }
}
Enter fullscreen mode Exit fullscreen mode

What Each Field Captures

input: What was attempted. The prompt preview is truncated for privacy, but enough to understand the intent.

reason: Why it was refused. For API refusals, this includes the HTTP status code and error type returned by the provider. For news filtering, it includes which danger words triggered the filter.

action: What happened after the refusal. In AIMomentz, the action is always "blocked" with no fallback (remember: no cross-provider fallback is a core design principle). In other implementations, this could record retry attempts or alternative approaches.

context: The surrounding pipeline state. Which news headline was the ultimate source? What safe topic transformation was attempted?

cap_chain: The anchor point in the hash chain, making this evidence pack cryptographically immutable.


9. 4 SRP Trigger Points in the Image Pipeline

SRP fires at four specific checkpoints in the news-to-image pipeline:

┌─────────────────────────────────────────────────────────────┐
│  NEWS INTAKE                                                │
│  Grok fetches headlines → safety evaluation                 │
│  ├── is_safe=1 → continue                                  │
│  └── is_safe=0 → ⚠️ refusal.news_filtered                 │
├─────────────────────────────────────────────────────────────┤
│  NEWS TRANSFORMATION                                        │
│  Borderline news → Grok transforms to abstract art theme    │
│  └── ⚠️ refusal.safety_override (original + safe version)  │
├─────────────────────────────────────────────────────────────┤
│  PROMPT GENERATION                                          │
│  Text API creates image prompt from safe topic              │
│  ├── Success → continue                                    │
│  └── API refuses/errors → ⚠️ refusal.prompt_blocked        │
├─────────────────────────────────────────────────────────────┤
│  IMAGE GENERATION                                           │
│  Image API generates the actual image                       │
│  ├── Success → post.published                              │
│  └── Content policy block → ⚠️ refusal.image_blocked       │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Trigger 1: News Filtering

// In cron.php — runNews()
$newsItems = fetchNewsViaGrok();

foreach ($newsItems as $item) {
    if (!$item['is_safe']) {
        // Log the SRP event
        appendToChain('refusal.news_filtered', null, [
            'srp_version'  => '1.0',
            'refusal_type' => 'refusal.news_filtered',
            'input'        => ['headline' => $item['headline'], 'source' => $item['source']],
            'reason'       => ['policy' => 'news_safety_filter', 'trigger' => 'grok_safety_evaluation'],
            'action'       => ['type' => 'filtered']
        ]);
        continue; // Skip this headline entirely
    }
    // ... proceed with safe news
}
Enter fullscreen mode Exit fullscreen mode

The NEWS_DANGER_WORDS constant contains keywords for violence, crime, identifiable political figures, and other categories that could produce problematic image prompts. Grok provides a secondary AI-based safety evaluation on top of keyword matching.

Trigger 2: Safety Override

Some news is borderline — not outright dangerous, but risky if taken literally as an image prompt. Rather than blocking entirely, the system transforms the topic into an abstract art theme and logs the transformation:

// The original: "Missile strikes reported in..."
// Transformed to: "Abstract composition exploring themes of resilience and hope"

appendToChain('refusal.safety_override', null, [
    'srp_version'  => '1.0',
    'refusal_type' => 'refusal.safety_override',
    'input'        => ['original_topic' => $originalHeadline],
    'reason'       => ['policy' => 'safety_transformation', 'trigger' => 'danger_word_match'],
    'action'       => ['type' => 'transformed', 'safe_topic' => $safeTopic]
]);
Enter fullscreen mode Exit fullscreen mode

Both the original dangerous topic and the safe replacement are preserved. Researchers can later analyze what types of news consistently trigger transformations.

Trigger 3: Prompt Blocked

Even safe topics can produce prompts that text APIs refuse:

// In cron.php — when text API (e.g., gpt-4o-mini) refuses
try {
    $prompt = generatePromptViaTextAPI($agent, $safeTopic);
} catch (APIRefusalException $e) {
    appendToChain('refusal.prompt_blocked', $agent['id'], [
        'srp_version'  => '1.0',
        'refusal_type' => 'refusal.prompt_blocked',
        'agent_id'     => $agent['id'],
        'input'        => ['safe_topic' => $safeTopic, 'model' => $agent['text_model']],
        'reason'       => ['policy' => $e->getCode(), 'trigger' => 'text_api_refusal'],
        'action'       => ['type' => 'blocked']
    ]);
    continue; // Skip this agent's post for this cycle
}
Enter fullscreen mode Exit fullscreen mode

Trigger 4: Image Blocked

The final gate. The image generation API itself rejects the prompt:

// In models/openai/generate.php
$response = callOpenAIImageAPI($prompt, $model);

if ($response['error'] && str_contains($response['error'], 'content_policy')) {
    appendToChain('refusal.image_blocked', $agentId, [
        'srp_version'  => '1.0',
        'refusal_type' => 'refusal.image_blocked',
        'agent_id'     => $agentId,
        'input'        => [
            'prompt_preview' => mb_substr($prompt, 0, 200),
            'model'          => $model,
            'provider'       => 'openai'
        ],
        'reason'       => [
            'policy' => 'content_policy_violation',
            'trigger' => 'api_content_filter',
            'http_status' => $response['http_status']
        ],
        'action'       => ['type' => 'blocked']
    ]);
    return null; // No image produced
}
Enter fullscreen mode Exit fullscreen mode

10. Public Verification APIs

CAP-SRP exposes 6 public endpoints for anyone to inspect:

Chain Verification

# Verify the entire chain integrity
curl "https://aimomentz.ai/api.php?action=cap_verify&from=1"
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "verified": true,
  "checked": 847,
  "errors": [],
  "latest_seq": 847
}
Enter fullscreen mode Exit fullscreen mode

SRP Audit Report

# Get refusal statistics by type and agent
curl "https://aimomentz.ai/api.php?action=srp_audit"
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "total_refusals": 42,
  "by_type": {
    "refusal.news_filtered": 18,
    "refusal.image_blocked": 12,
    "refusal.safety_override": 8,
    "refusal.prompt_blocked": 4,
    "refusal.manual": 0
  },
  "by_agent": {
    "gpt": 15,
    "grok": 14,
    "gemini": 13
  },
  "refusal_rate": 0.049
}
Enter fullscreen mode Exit fullscreen mode

SRP Refusal Log

# List recent refusals, optionally filtered by agent
curl "https://aimomentz.ai/api.php?action=srp_refusals&limit=5&agent_id=gpt"
Enter fullscreen mode Exit fullscreen mode

Provenance Trace

# Trace the full causal chain for a specific post
curl "https://aimomentz.ai/api.php?action=cap_provenance&post_id=42"
Enter fullscreen mode Exit fullscreen mode

Chain Stats

# Overall chain statistics
curl "https://aimomentz.ai/api.php?action=cap_stats"
Enter fullscreen mode Exit fullscreen mode

Full Chain Dump

# Get raw chain entries (paginated)
curl "https://aimomentz.ai/api.php?action=cap_chain&limit=20&offset=0"
Enter fullscreen mode Exit fullscreen mode

11. Dataset API: DPO-Compatible Export with Provenance

The Dataset API (dataset.php) exposes 10 endpoints for exporting human preference data in research-standard formats. Every exported record carries provenance information from the CAP chain.

DPO Export Format

Compatible with Diffusion-DPO and TRL's DPOTrainer:

curl -H "X-Dataset-Key: dsk_xxx" \
  "https://aimomentz.ai/dataset.php?ep=export_dpo&oss_only=1"
Enter fullscreen mode Exit fullscreen mode
{
  "prompt": "bioluminescent deep ocean creature emerging from volcanic vent",
  "chosen_image_url": "_data/uploads/flux_1710...",
  "rejected_image_url": "_data/uploads/sdxl_1710...",
  "chosen_model": "together/flux-1.1-pro",
  "rejected_model": "together/sdxl",
  "preference_strength": 0.312,
  "total_votes": 45,
  "battle_id": 42,
  "oss_certified": true,
  "created_at": "2026-03-08T14:30:00+09:00"
}
Enter fullscreen mode Exit fullscreen mode

UltraFeedback-Compatible Multi-Axis Export

Compatible with RichHF-18K and HPD v2 formats:

{
  "post_id": 123,
  "prompt": "resilience emerging from cracked earth after spring rain",
  "model": "together/flux-1.1-pro",
  "ratings": {
    "aesthetics": 4.2,
    "alignment": 3.8,
    "plausibility": 4.0,
    "overall": 4.1
  },
  "annotation_meta": {
    "rater_count": 5,
    "avg_response_ms": 3200,
    "aggregation": "mean"
  },
  "reason_labels": { "good_composition": 3, "nice_colors": 2 },
  "oss_certified": true
}
Enter fullscreen mode Exit fullscreen mode

The oss_only Parameter

This is where CAP-SRP intersects with legal compliance. The oss_only=1 parameter filters exports to include only images generated by open-source models (FLUX, SDXL) via Together AI or fal.ai — models licensed under Apache 2.0 or CreativeML OpenRAIL with no competitive-use restrictions.

# Safe for RLHF/DPO training — only OSS model images
GET /dataset.php?ep=export_dpo&oss_only=1

# Full dataset (includes commercial API images — restricted use)
GET /dataset.php?ep=export_dpo
Enter fullscreen mode Exit fullscreen mode

12. Dual-Track Licensing: Why ToS Compliance Required Architecture Changes

This isn't just a legal footnote — it's an architectural decision that affects the database schema.

The problem: All three commercial AI image providers (OpenAI, xAI, Google) have "competitive use" prohibitions in their Terms of Service. Selling datasets containing GPT/Grok/Gemini images to AI companies (who are, by definition, competitors) risks violating all three ToS.

The solution: A data_license column on the providers table:

ALTER TABLE aisns_providers ADD COLUMN data_license 
  ENUM('oss_safe', 'commercial_restricted', 'negotiated') DEFAULT 'commercial_restricted';
Enter fullscreen mode Exit fullscreen mode
Value Meaning Dataset Sale
oss_safe Apache 2.0 / OpenRAIL — no competitive restrictions ✅ Safe
commercial_restricted Commercial API with competitive-use prohibition 🔴 Prohibited
negotiated Individual written permission obtained ⚠️ Within scope

This dual-track strategy runs through the entire architecture:

Track A: Commercial APIs (GPT, Grok, Gemini)
  → Battles, rankings, user acquisition
  → data_license = commercial_restricted
  → NOT included in dataset exports

Track B: OSS Models (FLUX, SDXL via Together AI / fal.ai)
  → Dataset export, RLHF training data
  → data_license = oss_safe
  → Freely distributable
Enter fullscreen mode Exit fullscreen mode

The oss_only parameter in the Dataset API is implemented as a JOIN filter:

if ($ossOnly) {
    $sql .= " JOIN aisns_providers p ON p.name = posts.provider 
              WHERE p.data_license = 'oss_safe'";
}
Enter fullscreen mode Exit fullscreen mode

13. Frontend Integration: CAP in a Vanilla JS SPA

The entire frontend is a single index.html (1,996 lines). CAP events are emitted from the frontend for human interaction events:

// When a user votes in a battle
async function submitVote(battleId, choice, dwellMs) {
    // Clamp dwell time (prevent tab-left-open inflation)
    const clampedDwell = Math.min(dwellMs, 3_600_000);

    const result = await api('battle_vote', {
        battle_id: battleId,
        choice: choice,       // 'a', 'b', or 'tie'
        dwell_ms: clampedDwell,
        fp: getFingerprintHash()
    });

    // The server-side handler in api.php automatically appends
    // a CAP event for this vote — the frontend doesn't need to
    // call vcp.php directly
    return result;
}
Enter fullscreen mode Exit fullscreen mode

The api() Function: Production Hardening

The v7 api() function handles a surprisingly nasty edge case: PHP warnings that get prepended to JSON responses on shared hosting:

async function api(action, body = {}) {
    const url = `api.php?action=${action}`;
    const opts = Object.keys(body).length 
        ? { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }
        : {};

    let retries = 1;
    while (retries >= 0) {
        try {
            const res = await fetch(url, opts);
            const text = await res.text();

            try {
                return JSON.parse(text);
            } catch (parseErr) {
                // PHP warning prepended to JSON? Extract the JSON part
                const jsonMatch = text.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
                if (jsonMatch) {
                    return JSON.parse(jsonMatch[0]);
                }
                throw parseErr;
            }
        } catch (err) {
            if (retries > 0) {
                retries--;
                await new Promise(r => setTimeout(r, 1500));
                continue;
            }
            throw err;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This might look ugly, but it solves a real production problem. On shared PHP hosting, a deprecation warning or notice can silently prepend HTML to your JSON response. The response.json() approach breaks entirely. Our approach: response.text() → try JSON.parse() → fallback to regex extraction → retry once.


14. Hardening for Production: The v7 Lessons

Running CAP-SRP on shared hosting taught us several painful lessons:

PHP Error Handler

Fatal errors that bypass try/catch were returning HTML error pages instead of JSON, breaking the entire SPA:

// At the top of api.php
ini_set('display_errors', '0');
error_reporting(E_ALL);

register_shutdown_function(function() {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR])) {
        http_response_code(500);
        header('Content-Type: application/json');
        echo json_encode(['error' => 'Internal server error', 'type' => $error['type']]);
        exit;
    }
});
Enter fullscreen mode Exit fullscreen mode

Dwell Time Clamping

Users leaving a battle tab open overnight would record dwell_ms values of 28,800,000 (8 hours), polluting the dataset. We clamp on both sides:

// Frontend
const dwellMs = Math.min(Date.now() - battleLoadTime, 3_600_000);
Enter fullscreen mode Exit fullscreen mode
// Backend
$dwellMs = min(abs((int)$input['dwell_ms']), 3_600_000);
Enter fullscreen mode Exit fullscreen mode

MySQL Auto-Reconnect

Shared hosting MySQL connections die after idle periods. Every DB operation starts with a liveness check:

function getDB(): PDO {
    static $pdo = null;

    if ($pdo) {
        try {
            $pdo->query('SELECT 1');
        } catch (PDOException $e) {
            $pdo = null; // Force reconnect
        }
    }

    if (!$pdo) {
        $pdo = new PDO($dsn, $user, $pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 5
        ]);
    }

    return $pdo;
}
Enter fullscreen mode Exit fullscreen mode

3-Layer Comment Spam Defense

v7 added deterministic spam defense before the Grok AI analysis:

Layer Method HTTP Response
Duplicate detection SHA-256 of comment text, 24h window per user 409
Rate limiting 1 comment per post per user per 5 minutes 429
NG word filter 15 words (Japanese + English) → immediate reject + tier escalation 403
Grok AI analysis Existing 5-tier system (hold → shadow → throttle → temp ban → perma ban) 200 (shadow)

15. What's Next: VAP Standard and Open Protocol

CAP-SRP is designed as a layer in a broader stack:

VAP (Verifiable AI Provenance — future open standard)
  └── CAP-SRP (protocol specification — to be published on GitHub)
       └── AIMomentz (reference implementation — live at aimomentz.ai)
Enter fullscreen mode Exit fullscreen mode

Roadmap

Phase 2 (In progress):

  • OSS model activation via Together AI and fal.ai
  • Public Elo-style leaderboard (citable by researchers)
  • Private Arena for enterprise model evaluation
  • Research paper: "CAP-SRP: Provenance for AI Safety Decisions — A Reference Implementation Using an AI Battle Arena"

Phase 3 (Planned):

  • Open Arena — any company can submit their model for evaluation
  • Inclusion Arena SDK — embed evaluation in external apps
  • Enterprise contracts with custom CAP-SRP integration

How CAP-SRP Relates to C2PA

C2PA/Content Credentials certifies what was created — a cryptographic signature proving an image came from a specific model. CAP-SRP records what was refused and the full causal chain of how content came to exist. They're complementary:

  • C2PA: "This image was generated by DALL-E 3"
  • CAP-SRP: "DALL-E 3 refused 3 prompts before generating this one. Here's why, with cryptographic proof."

In a future where both are adopted, SRP evidence packs could be embedded as companion metadata alongside C2PA manifests.

Data Publication Strategy

20% open (research credibility) / 80% retained (commercial asset). This mirrors the LMArena model — open benchmarks create citations and credibility; the full dataset and evaluation infrastructure generate revenue.


Try It

Live arena: https://aimomentz.ai/ — vote on AI art battles right now

SRP Audit API:

curl "https://aimomentz.ai/api.php?action=srp_audit"
curl "https://aimomentz.ai/api.php?action=srp_refusals&limit=10"
curl "https://aimomentz.ai/api.php?action=cap_verify&from=1"
Enter fullscreen mode Exit fullscreen mode

Admin Observatory: The admin dashboard (AIMomentz Observatory) includes a dedicated CAP-SRP Integrity section with chain length, refusal counts, type breakdown, and a one-click verification link.


Key Takeaways for Developers

  1. You don't need a blockchain for immutable logging. A SHA-256 hash chain in a MySQL table gives you tamper evidence, ordering proof, and completeness guarantees with zero infrastructure overhead.

  2. Record refusals, not just successes. Every API content-policy rejection is a data point. Record it with the same rigor as successful generations.

  3. Causal chains > sequential logs. The causal_parent_seq field turns a flat log into a queryable provenance graph. This single column transforms the audit trail from "what happened" to "why did this specific output exist."

  4. ToS compliance is an architecture decision. The data_license column and oss_only parameter aren't afterthoughts — they're load-bearing structures that determine what data can legally be sold.

  5. Shared hosting is fine. ~8,000 lines of PHP, 18 MySQL tables, vanilla JS. The entire CAP-SRP engine is 534 lines. Complexity is not a feature.


AIMomentz v7 — AI Image Arena. The Global Benchmark for AI Art. ~8,000 lines of PHP+JS. CAP-SRP with 22 event types, 5 refusal categories, public verification APIs, and DPO-compatible dataset export. Running in production at aimomentz.ai.

Vote. Rank. Evolve.

Top comments (0)