SkillAuthorRegistry of AI Skills
Claude Agent Skills Registry · Technical overview

How SkillAuthor is built.

Implementation overview of the Claude Agent Skills Registry: the stack, the invariants we enforce, the architecture behind cryptographic proof of authorship for .mdc and SKILL.md files, and what is and isn't built. Source-of-truth for developers evaluating whether to depend on the service.

Stack and runtime

  • Runtime: Node.js 22, Next.js 16 App Router
  • Database: PostgreSQL 16 (self-hosted, Drizzle ORM)
  • Payments: Stripe Checkout, API version 2026-04-22.dahlia
  • PDF: @react-pdf/renderer 4.x, Times Roman + Helvetica, A4
  • Email: Cloudflare Email Sending Beta for transactional mail
  • Edge: Cloudflare DNS / CDN / TLS in front of a Caddy 2 instance terminating HTTP/2 + HTTP/3 with automatic Let's Encrypt
  • Host: Hetzner Cloud, EU region

Client-side SHA-256 hashing

The browser computes SHA-256 over the exact file bytes using crypto.subtle.digest("SHA-256", arrayBuffer) before submission. This gives the user instant feedback and lets the upload form display duplicate matches without first uploading. The server computes its own SHA-256 on receipt and that value is what gets recorded on the certificate — the client-side hash is only an indication during upload.

const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buf);
const hex = Array.from(new Uint8Array(digest))
  .map((b) => b.toString(16).padStart(2, "0"))
  .join("");

Atomic registration number minting

Numbers are minted inside the Stripe webhook handler in a single transaction:

return db.transaction(async (tx) => {
  const [row] = await tx
    .select({ id: skills.id,
              registrationNumber: skills.registrationNumber })
    .from(skills)
    .where(eq(skills.id, skillId))
    .for("update")
    .limit(1);

  if (!row) return { outcome: "not_found" };
  if (row.registrationNumber)
    return { outcome: "already_finalized",
             registrationNumber: row.registrationNumber };

  const [counter] = await tx
    .insert(registrationCounters)
    .values({ year, lastSeq: 1 })
    .onConflictDoUpdate({
      target: registrationCounters.year,
      set: { lastSeq: sql`${registrationCounters.lastSeq} + 1` },
    })
    .returning({ lastSeq: registrationCounters.lastSeq });

  const registrationNumber =
    `SA-${year}-${String(counter.lastSeq).padStart(6, "0")}`;

  await tx.update(skills).set({
    paymentStatus: "paid",
    registeredAt: now,
    registrationNumber,
  }).where(eq(skills.id, skillId));

  return { outcome: "finalized", registrationNumber };
});

Properties:

  • Two concurrent Stripe deliveries for the same session cannot both pass the registrationNumber IS NULL check — the second is serialised behind the row lock and returns already_finalized.
  • The counter is incremented exactly once per registration. Numbers can have gaps (abandoned checkout sessions) but never duplicates.
  • The handler returns 200 OK on already_finalized so Stripe stops retrying.

Webhook security and idempotency

Verification uses stripe.webhooks.constructEventAsync (SubtleCrypto under the hood, async-friendly). On failure the response is a generic 400 invalid_signature — the exception message is logged server-side but never echoed back, to avoid signature-introspection oracles.

5xx responses trigger Stripe's exponential retry (up to three days in live mode). Combined with the row-locked finalisation above, retries are safe.

Defence-in-depth IP allowlisting for Stripe's published webhook IPs is supported by an opt-in @stripeWebhook block in the Caddyfile.

PDF certificate generation

  • Pipeline: Drizzle row lookup → @react-pdf/renderer Document → renderToBuffer application/pdf stream
  • Typography: Times Roman + Times Italic for body; Courier for the registration number, hash, and timestamp blocks
  • Symbol: /public/symbol.png (512 × 512) embedded as a raster Image in the footer
  • QR code: generated inline via the qrcode library at 256 × 256, error correction level M, black-on-white, embedded above “Verify at” on every certificate
  • Cache: Cache-Control: public, max-age=31536000, immutable — each PDF URL is content-addressed by registration number, so immutability is safe

QR code on every certificate

Every certificate carries a QR that encodes https://skillauthor.com/v/<reg>. The same QR is available as a 1024 × 1024 standalone PNG at /api/certificate/<reg>/qr.

Specs:

  • Error correction: M (≈ 15 %), enough redundancy for print and paper damage
  • Module size: 32 px at 1024 × 1024
  • Quiet zone: 1 module
  • Colours: #000000 on #ffffff — scanner compatibility first

Independent verification

Public verification needs only shasum and the registration number:

$ shasum -a 256 inspector-agent.mdc
# compare to the fingerprint at /v/SA-2026-000142

# or programmatically:
$ curl -s https://skillauthor.com/api/register/check \
    -H 'Content-Type: application/json' \
    -d '{"sha256":"<your hash>"}'

The verification page at /v/<reg> does not require accounts, cookies, or JavaScript.

Data model and privacy

  • No user accounts. No password storage, no session cookies, no OAuth.
  • Email is never published. Stored only for receipt delivery and verification correspondence.
  • Web logs retained 30 days for security and abuse-prevention; deleted on a rolling basis.
  • Registration records retained indefinitely — that's the service. GDPR erasure requests replace the author name with “Author withdrawn” but preserve the hash, number, and timestamp.

See /privacy for the full data inventory.

Semantic near-duplicate detection (active)

After the registration is sealed, the webhook schedules a background check between 5 and 60 minutes later. The check is owned by a cron-invoked endpoint POST /api/jobs/near-dup-check; the request path never blocks on it, so checkout and PDF issuance are unaffected by OpenAI latency or outages.

  • Model: text-embedding-3-small · 1536 dimensions · input truncated to 24 000 chars
  • Index: pgvector ivfflat with vector_cosine_ops, lists = 100
  • Default alert threshold: cosine similarity ≥ 0.84. The threshold is per-receiver and raised by 0.04 (capped at 0.92) when a recipient flags an alert as a false positive
  • Only the older skill in a strongly-similar pair receives an alert — never the new submitter
  • Top 3 strongest matches surface per scan. Every match becomes an alert row with a per-alert false-positive token; recipients are deduplicated so a single registrant who owns multiple matched skills receives one email, not several
  • Retry policy: 5 attempts with a 1-hour lease between attempts; non-retryable errors (e.g. missing API key) move the task straight to failed
  • Failure mode: if OpenAI is unreachable, the task is rescheduled an hour later; the registration itself is unaffected

Roadmap

Considered, not started. We will not advertise them on the homepage until they ship.

  • Signed JSON attestations. A .json companion to the PDF, signed with SkillAuthor's Ed25519 key. Verifiable without the PDF rendering pipeline. Public key published at /.well-known/skillauthor.pub.
  • Authenticated per-author API. “List my registrations”, transfer ownership, revoke. Requires accounts; intentionally deferred.
  • Hash-only private tier. Pay to publish only the hash, not the bytes. Schema already has is_public; only the UI toggle and the registry filter are missing.
  • Merkle-rooted snapshots. Daily hash of (sha256, registered_at) rows, anchored in a public timestamp service (OpenTimestamps or a public git mirror). Long-term archival guarantee.

No date promises. Email [email protected] if you depend on a specific item — that's how priority is assigned.