Skip to main content
← All recipes
IntegrationIntermediate

Sign RAG-augmented outputs with source attribution

RAG pipelines compose AI output from retrieved documents. When that output is later challenged, you need to prove WHICH documents informed it. This recipe embeds source-document hashes in receipt metadata so the source chain is cryptographically attestable.

When to use this pattern

  • — Healthcare: clinical decision support drawing from EHR records, drug interaction databases, clinical guidelines
  • — Legal: brief drafting that pulls from case law, contracts, prior filings
  • — Finance: research outputs referencing internal docs, SEC filings, market data
  • — News/publishing: AI-assisted articles drawing from primary sources that must be attributable later

Pattern

Hash each retrieved document, sign the AI output with a manifest of source hashes in the prompt-hash field. When verifying later, re-fetch the sources and re-hash to prove they're unchanged.

import Anthropic from '@anthropic-ai/sdk'
import { CertNode } from '@certnode/sdk'
import crypto from 'crypto'

const claude = new Anthropic()
const cert = new CertNode({ apiKey: process.env.CERTNODE_API_KEY! })

interface RetrievedDoc {
  id: string
  title: string
  content: string
  source: string
}

async function answerWithProvenance(question: string) {
  // 1. Retrieve relevant documents from your vector store / search index
  const docs: RetrievedDoc[] = await retrieveRelevantDocs(question)

  // 2. Hash each retrieved doc — capture EXACT content used in this turn
  const sourceManifest = docs.map((d) => ({
    id: d.id,
    source: d.source,
    contentHash: crypto.createHash('sha256').update(d.content).digest('hex'),
  }))

  // 3. Build the RAG prompt
  const context = docs
    .map((d) => `<doc id="${d.id}" source="${d.source}">\n${d.content}\n</doc>`)
    .join('\n\n')

  // 4. Call the model
  const response = await claude.messages.create({
    model: 'claude-opus-4-7',
    max_tokens: 2048,
    messages: [
      {
        role: 'user',
        content: `Sources:\n${context}\n\nQuestion: ${question}\n\nAnswer using only the sources.`,
      },
    ],
  })
  const aiOutput = response.content.find((c: any) => c.type === 'text')?.text ?? ''

  // 5. Sign the AI output with the source manifest in promptHash
  // We hash the manifest (not embed it raw) because promptHash is a single
  // string field. Storing the manifest in your own DB keyed by receipt ID
  // lets verifiers reconstruct which sources were used.
  const manifestHash = crypto
    .createHash('sha256')
    .update(JSON.stringify(sourceManifest))
    .digest('hex')

  const signed = await cert.signAIOutput({
    output: aiOutput,
    model: 'claude-opus-4-7',
    provider: 'anthropic',
    promptHash: `rag-manifest=${manifestHash}`,
  })

  // 6. Store the full manifest server-side keyed by the receipt ID
  await db.ragManifests.insert({
    receiptId: signed.receiptId,
    manifest: sourceManifest,
    question,
    createdAt: new Date(),
  })

  return {
    answer: aiOutput,
    receiptId: signed.receiptId,
    verifyUrl: signed.verifyUrl,
    sourceManifest, // returned to the client for inline source citation
  }
}

Verifying the chain later

When asked "which sources produced this answer," reconstruct the proof:

async function verifyRagChain(receiptId: string) {
  // 1. Verify the receipt itself
  const receipt = await cert.verify({ receiptId })
  if (!receipt.valid) throw new Error('Receipt invalid')

  // 2. Pull the stored manifest
  const stored = await db.ragManifests.findOne({ receiptId })
  if (!stored) throw new Error('No manifest stored for this receipt')

  // 3. Re-hash the stored manifest, compare to receipt's promptHash
  const recomputed = crypto
    .createHash('sha256')
    .update(JSON.stringify(stored.manifest))
    .digest('hex')

  const promptHashFromReceipt = receipt.metadata.promptHash.replace('rag-manifest=', '')
  if (recomputed !== promptHashFromReceipt) {
    throw new Error('Manifest tampered with since signing')
  }

  // 4. For each source doc in the manifest, re-fetch and re-hash
  const sourceVerifications = await Promise.all(
    stored.manifest.map(async (entry: any) => {
      const currentContent = await fetchSource(entry.source, entry.id)
      const currentHash = crypto.createHash('sha256').update(currentContent).digest('hex')
      return {
        id: entry.id,
        source: entry.source,
        unchanged: currentHash === entry.contentHash,
      }
    })
  )

  return {
    receiptValid: receipt.valid,
    signedAt: receipt.signedAt,
    bitcoinAnchor: receipt.timestamps.bitcoin?.status,
    sourceVerifications, // per-source: unchanged or modified
  }
}

What this proves under FRE 902(13)/(14)

The receipt establishes:

  • — The AI output existed in its exact form at the timestamp on the receipt (CertNode + RFC 3161 + Bitcoin anchor)
  • — A specific bundle of source documents (identified by their hashes) was used to produce that output — the manifest is bound to the receipt by its hash inpromptHash
  • — Whether each source has been modified since the AI used it (re-fetch + re-hash produces a fresh comparison)

This is what makes RAG outputs defensible against "you fabricated this after the fact" and "you used different sources than you claim." The manifest-hash binding makes both attacks computationally infeasible.