Verify a SongForgeAI seal in 60 seconds.
Every /api/v1/score response carries an ed25519 signature over the rubric version + model + temperature + build SHA. Anyone with the public key (served at /.well-known/songforgeai-pubkey.json) can verify a score independently. No SongForgeAI infrastructure required; no trust assumed. Math, not promises.
1. Try it now (browser)
Paste a seal + signature + public key from any score response, or click Use the bundled example to load a known-good fixture. The verification runs entirely in your browser — we never see your seal.
2. Verify from a shell
One-liner: fetch a score, fetch the public key, run ed25519 verification with @noble/ed25519.
# 1. Score some lyrics + capture the seal block
curl -s -X POST https://songforgeai.com/api/v1/score \
-H "Authorization: Bearer $SFAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"lyrics": "..."}' \
| tee score.json
# 2. Pull the active public key
curl -s https://songforgeai.com/.well-known/songforgeai-pubkey.json > pubkey.json
# 3. Verify (pure node, no extra packages)
node -e "
const { verify } = require('@noble/ed25519');
const { sha512 } = require('@noble/hashes/sha2.js');
require('@noble/ed25519').hashes.sha512 = (m) => sha512(m);
const score = require('./score.json');
const pubkey = require('./pubkey.json');
const seal = { ...score.seal };
const sig = seal.signature;
delete seal.signature;
const canonical = (o) => o === null || typeof o !== 'object'
? JSON.stringify(o)
: Array.isArray(o)
? '[' + o.map(canonical).join(',') + ']'
: '{' + Object.keys(o).sort().map(k =>
JSON.stringify(k) + ':' + canonical(o[k])
).join(',') + '}';
const message = new TextEncoder().encode(canonical(seal));
const sigBytes = Uint8Array.from(atob(sig.signature), c => c.charCodeAt(0));
const pkBytes = Buffer.from(pubkey.current.keyHex, 'hex');
verify(sigBytes, message, pkBytes).then(ok =>
console.log(ok ? '✓ valid' : '✗ INVALID')
);
"3. Verify from TypeScript / JavaScript
Use @songforgeai/client to fetch the score, then verify with @noble/ed25519 (a tiny zero-dep crypto library). The SDK stays dependency-free; you opt in to crypto only when you want it.
import { SongForgeAI } from '@songforgeai/client';
import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha2';
ed.hashes.sha512 = (m) => sha512(m);
const sf = new SongForgeAI({ apiKey: process.env.SFAI_API_KEY! });
const score = await sf.score({ lyrics: '...', genre: 'country' });
const pubkey = await fetch(
'https://songforgeai.com/.well-known/songforgeai-pubkey.json'
).then((r) => r.json());
const canonical = (o) => o === null || typeof o !== 'object'
? JSON.stringify(o)
: Array.isArray(o)
? '[' + o.map(canonical).join(',') + ']'
: '{' + Object.keys(o).sort().map((k) =>
JSON.stringify(k) + ':' + canonical(o[k])
).join(',') + '}';
const sealCopy = { ...score.seal };
const sig = sealCopy.signature;
delete sealCopy.signature;
const message = new TextEncoder().encode(canonical(sealCopy));
const sigBytes = Uint8Array.from(atob(sig.signature), (c) => c.charCodeAt(0));
const pkBytes = Buffer.from(pubkey.current.keyHex, 'hex');
const ok = await ed.verify(sigBytes, message, pkBytes);
console.log(ok ? 'verified' : 'rejected');What gets signed (and what doesn't)
The signature covers the seal block only: rubric version, model, temperature, build SHA, build number. NOT the individual metric values, NOT the lyrics, NOT the wounds.
Why: the seal is the recipe. If two third parties both sign the same recipe and run it, they should get the same numbers (or near-same — temperature adds bounded variance). Verifying the seal = verifying that this score came out of this recipe. Verifying the metric numbers themselves is a separate problem (re-run the model, compare outputs).
The full pipeline contract is documented at /scoring/standard/whitepaper (CC BY 4.0). The model card lives at /scoring/standard/model-card.