Verifiable provenance badges for AI-generated content.
Stamp AI-generated text with cryptographic metadata and embed a clickable badge anywhere that content appears. Anyone can click the badge to verify the model, timestamp, and content integrity.
This post was written with AI assistance.
<a href="https://provenancestamp.dev/verify/01JQXYZ...">🛡 AI Generated</a>
C2PA solves provenance for images, video, and audio. ProvenanceStamp does the same for text.
npm install @provenancestamp/sdkimport { ProvenanceStamp } from '@provenancestamp/sdk';
const ps = new ProvenanceStamp({ apiKey: process.env.PROVENANCE_API_KEY });
const stamp = await ps.stamp({
content: aiGeneratedText,
model: 'claude-sonnet-4-20250514',
provider: 'anthropic',
generatedAt: new Date().toISOString(),
});
console.log(stamp.verifyUrl); // https://provenancestamp.dev/verify/01JQXYZ...
console.log(stamp.badges.html); // <a href="...">🛡 AI Generated</a>That's it. Paste the badge HTML wherever the content appears.
When you call ps.stamp(), the SDK:
- Normalizes the content (NFC, trim, collapse whitespace) and computes
SHA-256(content) - Builds a deterministic metadata bundle (sorted keys) and computes
SHA-256(metadata) - Generates a ULID stamp ID (sortable, unique, no external deps)
- Computes
HMAC-SHA256(apiKey, stampId + contentHash + metadataHash)as a signature - POSTs the record to the verification service
- Returns badge snippets in HTML, Markdown, JSON-LD, and React formats
When someone clicks the badge, they land on the verification page showing the full provenance chain.
Content integrity: Call ps.verifyContent(text, stampId) to confirm the text matches the stored hash — detecting any modifications made after stamping.
| Option | Type | Description |
|---|---|---|
apiKey |
string |
Your API key. Falls back to PROVENANCE_API_KEY env var. |
serviceUrl |
string |
Verification service URL. Defaults to https://provenancestamp.dev. |
interface StampInput {
content: string; // required — the AI-generated text
model: string; // required — e.g. "claude-sonnet-4-20250514"
provider?: string; // e.g. "anthropic", "openai"
promptFingerprint?: string; // SHA-256 of the prompt (use ps.hashPrompt())
temperature?: number;
generatedAt: string; // required — ISO 8601 timestamp
confidence?: number; // 0–1
author?: string; // person or org that requested generation
customMetadata?: Record<string, string>;
}
interface StampResult {
id: string; // ULID stamp ID
contentHash: string; // SHA-256 of normalized content
metadataHash: string; // SHA-256 of metadata bundle
signature: string; // HMAC-SHA256 signature
stampedAt: string; // ISO 8601 timestamp from the service
verifyUrl: string; // https://provenancestamp.dev/verify/{id}
badges: {
html: string; // inline HTML badge
markdown: string; // Markdown image link
jsonLd: string; // <script type="application/ld+json"> block
react: string; // copy-pasteable React component
};
}Hash a prompt string to produce a promptFingerprint. The original prompt is never stored — only its SHA-256 hash.
Fetch and verify a stamp record. Returns { valid, stamp, error? }.
Verify a stamp and check that the provided content matches the stored hash. Returns { valid, contentMatch, stamp, error? }.
<!-- compact (default) -->
<a href="https://provenancestamp.dev/verify/01JQ..." ...>🛡 AI Generated</a>
<!-- minimal -->
<a href="...">🛡 AI Verified</a>
<!-- full -->
<a href="...">🛡 Claude Sonnet 4 · Mar 2026</a>Generate with options.style and options.theme ('light' | 'dark'):
stamp.badges.html // default: compact, light
generateHtmlBadge(id, url, input, { style: 'full', theme: 'dark' })[](https://provenancestamp.dev/verify/01JQ...)<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CreativeWork",
"identifier": "01JQ...",
"generator": { "@type": "SoftwareApplication", "name": "claude-sonnet-4-20250514" },
...
}
</script>stamp.badges.react contains a copy-pasteable React component with no dependencies (all styles inline).
import Anthropic from '@anthropic-ai/sdk';
import { ProvenanceStamp } from '@provenancestamp/sdk';
const client = new Anthropic();
const ps = new ProvenanceStamp({ apiKey: process.env.PROVENANCE_API_KEY });
const response = await client.messages.create({ model: 'claude-sonnet-4-20250514', ... });
const content = response.content[0].text;
const stamp = await ps.stamp({
content,
model: 'claude-sonnet-4-20250514',
provider: 'anthropic',
promptFingerprint: await ps.hashPrompt(myPrompt),
generatedAt: new Date().toISOString(),
});import OpenAI from 'openai';
import { ProvenanceStamp } from '@provenancestamp/sdk';
const openai = new OpenAI();
const ps = new ProvenanceStamp({ apiKey: process.env.PROVENANCE_API_KEY });
const completion = await openai.chat.completions.create({ model: 'gpt-4o', ... });
const content = completion.choices[0].message.content ?? '';
const stamp = await ps.stamp({
content, model: 'gpt-4o', provider: 'openai',
generatedAt: new Date().toISOString(),
});See examples/ for more: blog post injection, Express middleware.
npm install -g provenancestamp
# Set up API key
provenancestamp init
# Stamp a file
provenancestamp stamp post.md --model claude-sonnet-4-20250514 --author "My Blog"
# Stamp from stdin
cat output.txt | provenancestamp stamp --stdin --model gpt-4o --output markdown
# Verify a stamp
provenancestamp verify 01JQXYZ...
# Verify a stamp and check file integrity
provenancestamp verify 01JQXYZ... post.mdContent Hash = SHA-256(NFC(trim(collapse_whitespace(content))))
Metadata Bundle = JSON.stringify(sort_keys({ content_hash, model, provider,
prompt_fingerprint, temperature, confidence,
generated_at, author, custom_metadata }))
Metadata Hash = SHA-256(Metadata Bundle)
Stamp Signature = HMAC-SHA256(key=api_key, msg=stamp_id + content_hash + metadata_hash)
The signature is stored with the stamp. It proves to the key holder that the record wasn't modified — but it's symmetric, so public trustless verification requires hitting the service.
Roadmap: Asymmetric Ed25519 signatures for trustless verification without needing the service online.
git clone https://github.com/provenancestamp/provenancestamp
cd provenancestamp
pnpm install
# Set environment variables
cp .env.example .env
# Edit .env: set MASTER_API_KEY
# Run the server
pnpm --filter @provenancestamp/server devThe server uses SQLite (no separate database to set up). To use Postgres later, swap better-sqlite3 for a Postgres client in packages/server/src/db/schema.ts.
Create your first API key:
curl -X POST http://localhost:3000/api/v1/keys \
-H "X-API-Key: $MASTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My App"}'The key field in the response is your API key. It's shown only once.
provenancestamp/
├── packages/
│ ├── sdk/ @provenancestamp/sdk — stamp creation, badge generation, verification
│ ├── server/ Hono API + SQLite + SSR verification page
│ └── cli/ provenancestamp CLI (Commander.js)
└── examples/ Working integration examples
- Ed25519 asymmetric signatures for trustless public verification
- C2PA interoperability (embed stamp as C2PA assertion)
- Browser extension for auto-verification of badges on any page
- WordPress / Ghost plugins
- Streaming content support (stamp as chunks arrive)
MIT