Skip to main content

Implementor's Guide

AIPM 1.1 · 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.1 mark consists of a QR code whose payload is an HTTPS URL with provenance metadata encoded in the hash fragment. 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.1 URL

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

/**
 * Build a plain (uncompressed) AIPM 1.1 URL.
 * All metadata is encoded as URL hash fragment params.
 * The hash is never sent to any server.
 */
function buildAIPMUrl(options) {
  const {
    base = 'https://ai-pm.pages.dev/1.1/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.1');
  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 (doc)   params.set('doc', doc);
  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://ai-pm.pages.dev/1.1/aipm/#v=1.1&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.1 URL

For longer context, use the z param: deflate-raw compressed, base64url-encoded JSON. Auto-switch to compression when plain URL exceeds ~1,100 characters.

/**
 * 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://ai-pm.pages.dev/1.1/aipm/',
    ...meta
  } = options;

  // Build payload — omit empty values
  const payload = { v: '1.1' };
  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.doc)   payload.doc   = meta.doc;
  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.

const QR_BYTE_LIMIT = 1273; // QR v40 Level H binary mode

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

async function buildOptimalAIPMUrl(options) {
  const base = options.base || 'https://ai-pm.pages.dev/1.1/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.1 URL

/**
 * Parse an AIPM 1.1 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://ai-pm.pages.dev/1.1/aipm/#v=1.1&model=Claude+Sonnet+4.6&role=prompted%2Breviewed&date=2026-05-03T14%3A30-04%3A00&show=1&ctx=Blog+post');
// → { v: '1.1', model: 'Claude Sonnet 4.6', role: 'prompted+reviewed', ... }

2.2 — Parse a compressed AIPM 1.1 URL

/**
 * Parse an AIPM 1.1 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.1 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');
}

Conformance Checklist

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