Skip to main content

Implementor's Guide

AIPM 1.2 · For developers and AI systems building tools that generate or consume AIPM marks. All examples are working JavaScript suitable for browser and Node.js environments.

Overview

An AIPM 1.2 mark consists of a URL with provenance metadata encoded in the hash fragment. From that URL, three visual artifacts can be derived: a QR mark for physical and print contexts, a badge for digital contexts like GitHub and email, and the provenance page that both link to. Implementations fall into two categories:

Both must handle plain (uncompressed) and compressed (z param) formats. Both must apply security validation before rendering any URL-derived data.

Part 1: Generating AIPM URLs

1.1 — Build a plain AIPM 1.2 URL

For short context that fits within ~200 characters, use individual params in the hash fragment.

/**
 * Build a plain (uncompressed) AIPM 1.2 URL.
 * All metadata is encoded as URL hash fragment params.
 * The hash is never sent to any server.
 */
function buildAIPMUrl(options) {
  const {
    base = 'https://aipmq.org/1.2/aipm/',
    model = '',
    role = 'prompted',
    date = '', // ISO 8601: 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM±HH:MM' with UTC offset (recommended)
    ctx = '',
    lang = '',
    src = '',
    doc = '',
    prev = '',
    show = false,
  } = options;

  const params = new URLSearchParams();
  params.set('v', '1.2');
  if (model) params.set('model', model);
  if (role)  params.set('role', role);
  if (date)  params.set('date', date);
  if (ctx)   params.set('ctx', ctx);
  if (lang)  params.set('lang', lang);
  if (src)   params.set('src', src);
  if (hs)    params.set('hs', hs);
  if (type)  params.set('type', type);
  if (doc)   params.set('doc', doc);
  if (hd)    params.set('hd', hd);
  if (prev)  params.set('prev', prev);
  if (show)  params.set('show', '1');

  return base + '#' + params.toString();
}

// Example usage:
const url = buildAIPMUrl({
  model: 'Claude Sonnet 4.6',
  role: 'prompted+reviewed',
  date: '2026-05-03T14:30-04:00', // datetime with UTC offset recommended
  show: true,
  ctx: 'Blog post about AI transparency',
});
// → https://aipmq.org/1.2/aipm/#v=1.2&model=Claude+Sonnet+4.6&role=prompted%2Breviewed&date=2026-05-03T14%3A30-04%3A00&show=1&ctx=Blog+post+about+AI+transparency

1.2 — Build a compressed AIPM 1.2 URL

For longer context, use the z param: deflate-raw compressed, base64url-encoded JSON. Auto-switch to compression when the ctx field exceeds 200 characters, or when the plain URL exceeds the 1,000-byte QR limit.

/**
 * Compress all metadata into the z param.
 * Uses browser-native CompressionStream — no library needed.
 * For Node.js 18+, use the same API (available globally).
 */
async function buildCompressedAIPMUrl(options) {
  const {
    base = 'https://aipmq.org/1.2/aipm/',
    ...meta
  } = options;

  // Build payload — omit empty values
  const payload = { v: '1.2' };
  if (meta.model) payload.model = meta.model;
  if (meta.role)  payload.role  = meta.role;
  if (meta.date)  payload.date  = meta.date;
  if (meta.ctx)   payload.ctx   = meta.ctx;
  if (meta.lang)  payload.lang  = meta.lang;
  if (meta.src)   payload.src   = meta.src;
  if (meta.hs)    payload.hs    = meta.hs;
  if (meta.type)  payload.type  = meta.type;
  if (meta.doc)   payload.doc   = meta.doc;
  if (meta.hd)    payload.hd    = meta.hd;
  if (meta.prev)  payload.prev  = meta.prev;
  if (meta.show)  payload.show  = 1;

  // TextEncoder → CompressionStream → base64url
  const bytes = new TextEncoder().encode(JSON.stringify(payload));
  const cs = new CompressionStream('deflate-raw');
  const writer = cs.writable.getWriter();
  writer.write(bytes);
  writer.close();
  const compressed = await new Response(cs.readable).arrayBuffer();
  const b64 = btoa(String.fromCharCode(...new Uint8Array(compressed)));
  const z = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

  return base + '#z=' + z;
}

// Example usage:
const url = await buildCompressedAIPMUrl({
  model: 'Claude Sonnet 4.6',
  role: 'prompted+edited',
  date: '2026-05-03T14:30-04:00',
  ctx: 'A longer context description that benefits from compression — more than 200 characters can be encoded this way...',
});

1.3 — Auto-switching generator

The recommended approach: try plain first, switch to compressed if the URL exceeds the QR byte limit. Append qr=0 when compressed URL still exceeds the limit.

// AIPM conservative QR byte limit.
// The theoretical QR v40 Level H maximum is 1,273 bytes (binary mode),
// but real-world QR encoders and scanners are more reliable below 1,000 bytes.
// Implementors MUST use 1,000 (not 1,273) for conformance with AIPM 1.2.
const QR_BYTE_LIMIT = 1000;

function urlByteLength(str) {
  return new TextEncoder().encode(str).length;
}

async function buildOptimalAIPMUrl(options) {
  const base = options.base || 'https://aipmq.org/1.2/aipm/';

  // Try plain first
  const plainUrl = buildAIPMUrl(options);
  if (urlByteLength(plainUrl) <= QR_BYTE_LIMIT) {
    return { url: plainUrl, compressed: false, qrPossible: true };
  }

  // Try compressed
  const compressedUrl = await buildCompressedAIPMUrl(options);
  if (urlByteLength(compressedUrl) <= QR_BYTE_LIMIT) {
    return { url: compressedUrl, compressed: true, qrPossible: true };
  }

  // Still too long — add qr=0 flag
  const overflowUrl = await buildCompressedAIPMUrl({ ...options, qr: 0 });
  return { url: overflowUrl, compressed: true, qrPossible: false };
}

// Usage:
const result = await buildOptimalAIPMUrl({
  model: 'Claude Sonnet 4.6',
  role: 'prompted+reviewed',
  ctx: 'Very long context...',
});

if (result.qrPossible) {
  renderQrCode(result.url); // render QR
} else {
  displayAsLink(result.url); // display as text link
  // result.url contains qr=0 flag for downstream systems
}

1.4 — Datetime with UTC offset

When recording a precise timestamp, include the UTC offset so the time is unambiguous.

/**
 * Get the current datetime as an ISO 8601 string with UTC offset.
 * Format: YYYY-MM-DDTHH:MM+HH:MM
 * Example: 2026-05-03T14:30-07:00
 */
function getCurrentDatetimeWithOffset() {
  const now = new Date();
  const pad = n => String(n).padStart(2, '0');
  const off = -now.getTimezoneOffset(); // getTimezoneOffset returns inverted offset
  const sign = off >= 0 ? '+' : '-';
  const absOff = Math.abs(off);
  const offStr = sign + pad(Math.floor(absOff / 60)) + ':' + pad(absOff % 60);

  return now.getFullYear() + '-' +
    pad(now.getMonth() + 1) + '-' +
    pad(now.getDate()) + 'T' +
    pad(now.getHours()) + ':' +
    pad(now.getMinutes()) + offStr;
}

// The date field in the AIPM URL:
// When this provenance record was established —
// specifically, when the described human role was last applicable.
const date = getCurrentDatetimeWithOffset();
// → "2026-05-03T14:30-07:00"

Part 2: Consuming AIPM URLs

2.1 — Parse a plain AIPM 1.2 URL

/**
 * Parse an AIPM 1.2 URL (plain format).
 * Returns a metadata object or null if not valid AIPM.
 */
function parseAIPMUrl(url) {
  try {
    const parsed = new URL(url);
    const hash = parsed.hash.slice(1); // remove leading #
    if (!hash) return null;

    const params = new URLSearchParams(hash);

    // Check for compressed format
    if (params.has('z')) {
      throw new Error('Use parseAIPMUrlCompressed() for z param');
    }

    const v = params.get('v');
    if (!v) return null; // not an AIPM URL

    return {
      v,
      model: params.get('model') || '',
      role:  params.get('role')  || '',
      date:  params.get('date')  || '',
      ctx:   params.get('ctx')   || '',
      lang:  params.get('lang')  || '',
      src:   params.get('src')   || '',
      doc:   params.get('doc')   || '',
      prev:  params.get('prev')  || '',
      show:  params.get('show')  === '1',
      qr:    params.get('qr')    === '0' ? 0 : undefined,
    };
  } catch(e) { return null; }
}

// Example:
const meta = parseAIPMUrl('https://aipmq.org/1.2/aipm/#v=1.2&model=Claude+Sonnet+4.6&role=prompted%2Breviewed&date=2026-05-03T14%3A30-04%3A00&show=1&ctx=Blog+post');
// → { v: '1.2', model: 'Claude Sonnet 4.6', role: 'prompted+reviewed', ... }

2.2 — Parse a compressed AIPM 1.2 URL

/**
 * Parse an AIPM 1.2 URL with compressed z param.
 * Uses browser-native DecompressionStream.
 */
async function parseAIPMUrlCompressed(url) {
  try {
    const parsed = new URL(url);
    const hash = parsed.hash.slice(1);
    const params = new URLSearchParams(hash);
    const z = params.get('z');
    if (!z) return null;

    // base64url → base64 → Uint8Array
    const b64 = z.replace(/-/g, '+').replace(/_/g, '/');
    const binary = atob(b64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);

    // Decompress
    const ds = new DecompressionStream('deflate-raw');
    const writer = ds.writable.getWriter();
    writer.write(bytes);
    writer.close();
    const buf = await new Response(ds.readable).arrayBuffer();

    // Parse JSON
    return JSON.parse(new TextDecoder().decode(buf));
  } catch(e) {
    console.error('AIPM decompression failed:', e);
    return null;
  }
}

/**
 * Parse any AIPM 1.2 URL — auto-detects format.
 */
async function parseAIPMUrlAuto(url) {
  try {
    const parsed = new URL(url);
    const hash = parsed.hash.slice(1);
    const params = new URLSearchParams(hash);
    if (params.has('z')) {
      return await parseAIPMUrlCompressed(url);
    }
    return parseAIPMUrl(url);
  } catch(e) { return null; }
}

2.3 — Security: validate URLs before rendering

Always validate src, doc, and prev params before rendering as HTML href attributes. The escHtml() function prevents HTML injection but does not block javascript: or data: scheme URLs.

/**
 * Validate a URL from AIPM metadata before rendering as href.
 * Returns the URL if safe, null otherwise.
 */
function safeAIPMUrl(val) {
  if (!val || typeof val !== 'string') return null;
  try {
    const u = new URL(val);
    if (u.protocol !== 'https:' && u.protocol !== 'http:') return null;
    return val;
  } catch(e) { return null; }
}

/**
 * Escape HTML to prevent injection when inserting user data into DOM.
 */
function escHtml(str) {
  const d = document.createElement('div');
  d.appendChild(document.createTextNode(String(str)));
  return d.innerHTML;
}

// Usage — ALWAYS validate before rendering link fields:
function renderProvRow(label, value, isLink) {
  if (!value) return '';
  const displayVal = isLink
    ? (() => {
        const safe = safeAIPMUrl(value);
        return safe
          ? `<a href="${escHtml(safe)}" target="_blank" rel="noopener noreferrer">${escHtml(safe)}</a>`
          : `${escHtml(value)} (invalid URL)`;
      })()
    : escHtml(value);
  return `<div class="prov-row"><span>${escHtml(label)}</span>${displayVal}</div>`;
}

2.4 — Detect qr=0 (URL-only records)

/**
 * Check whether an AIPM URL is marked as URL-only (no QR code exists).
 * Automated systems should use this to decide whether to render a QR code
 * or fall back to displaying the URL as a text link.
 */
async function isQRPossible(url) {
  const meta = await parseAIPMUrlAuto(url);
  if (!meta) return false;
  return meta.qr !== 0 && meta.qr !== '0';
}

// Usage in a pipeline:
const aipmUrl = generateAIPMUrl(myContent);
if (await isQRPossible(aipmUrl)) {
  await renderQRCode(aipmUrl);
} else {
  displayAsTextLink(aipmUrl, 'View AI Provenance Record');
}

Part 3: AIPM 1.2 — New Features

A Python reference implementation is available for generating AIPM 1.2 URLs programmatically — useful for CI/CD pipelines, build scripts, and automated publishing workflows. No external dependencies; stdlib only.

python3 aipm.py generate --role prompted+reviewed --model "Claude Sonnet 4.6" --ctx "Blog post"
python3 aipm.py hash article.pdf
python3 aipm.py parse "https://aipmq.org/1.2/aipm/#..."

Download aipm.py →

3.1 — File integrity hashing (hs / hd)

Hash the content file (src) and/or Full Context Document (doc) so anyone can verify they are unchanged. Both are optional and may be set without the corresponding URL.

// Browser — SHA-256 via Web Crypto API (HTTPS / localhost only)
// Returns W3C SRI format: "sha256-<base64url>" (50 chars)
async function sha256SRI(file) {
  const buf     = await file.arrayBuffer();
  const hashBuf = await crypto.subtle.digest('SHA-256', buf);
  const arr     = new Uint8Array(hashBuf);
  let binary = '';
  for (let i = 0; i < arr.length; i++) binary += String.fromCharCode(arr[i]);
  return 'sha256-' + btoa(binary).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}

const hs = await sha256SRI(contentFile);   // e.g. "sha256-uU0nuZNNPgilLlLX…" (50 chars)
const hd = await sha256SRI(contextFile);
params.set('hs', hs);
params.set('hd', hd);

The file content is not normalised before hashing — the hash is computed over the raw file bytes as stored. For text files, CRLF vs LF line endings produce different hashes of the same logical content; the display page offers line-ending variant checking on mismatch. The hash value itself is stored in W3C Subresource Integrity (SRI) format: sha256-<base64url> (50 chars) — not raw hex.

Terminal equivalent (files >2 GB, or non-browser contexts). Terminal tools output hex (64 lowercase hex chars) — paste directly into the generator or verification paste field; the generator auto-converts to SRI format. Get-FileHash requires Windows 10+ / PowerShell 5.1+.

Mac / Linux:   shasum -a 256 filename
Windows 10+:   Get-FileHash filename -Algorithm SHA256

3.2 — Content type (type)

Declare the media type of the content. Recommended for EU AI Act Article 50 compliance — disclosure obligations differ by content type.

// Valid values: text | code | image | audio | video | multimodal
const VALID_TYPES = new Set(['text','code','image','audio','video','multimodal']);
if (VALID_TYPES.has(meta.type)) params.set('type', meta.type);

// EU Act Article 50 applies to these types when AI-generated:
const EU_ACT_50_TYPES = ['text','image','audio','video'];

3.3 — Automated role

Use role=automated when AI generated content without human involvement — publishing pipelines, automated summaries, etc. Use org to identify the responsible party.

const mark = buildAIPMUrl({
  role: 'automated',
  model: 'GPT-4o',
  type: 'text',
  org: 'Acme Publishing Inc.',
  date: new Date().toISOString(),
  ctx: 'Automated news summary',
  show: 1,  // Show mode → "AI" on QR mark line 3
});
// Badge renders as: AIPM v1.2 | AI

Conformance Checklist

Use this checklist to verify your AIPM 1.2 implementation. Check each item when satisfied. Progress is not saved — this is a self-assessment tool.