Skip to main content

What You’ll Build

A contract analysis pipeline that:
  1. Uploads contracts to a searchable vault
  2. Extracts key terms (parties, dates, amounts, obligations)
  3. Identifies risky clauses with severity ratings
  4. Compares similar clauses across multiple contracts
  5. Generates a formatted risk report
Time to complete: 25 minutes

Architecture

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│   Contract   │ ──▶ │     OCR      │ ──▶ │    Vault     │
│    Upload    │     │  Processing  │     │   Storage    │
└──────────────┘     └──────────────┘     │  + Indexing  │
                                          └──────┬───────┘

┌──────────────┐     ┌──────────────┐     ┌──────▼───────┐
│  PDF Report  │ ◀── │ LLM Analysis │ ◀── │  Vault       │
│  (Format)    │     │ Key Terms +  │     │  Search      │
│              │     │ Risk Flags   │     │              │
└──────────────┘     └──────────────┘     └──────────────┘

Prerequisites

  • Case.dev API key (get one here)
  • Contract documents (PDF, DOCX, or images)

Step 1: Create a Contract Vault

Set up a vault to store and index your contracts:
import Casedev from 'casedev';

const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });

const vault = await client.vault.create({
  name: 'Contracts — Q1 2024'
});

console.log(`Vault ID: ${vault.id}`);

Step 2: Upload and Index a Contract

Upload a contract, run OCR if needed, and index it for search:
import fs from 'fs';

async function uploadContract(
  vaultId: string,
  filePath: string,
  metadata?: Record<string, string>
) {
  const filename = filePath.split('/').pop()!;
  const contentType = filename.endsWith('.pdf') ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';

  // 1. Upload to vault
  const upload = await client.vault.upload(vaultId, {
    filename,
    contentType,
    metadata: {
      type: 'contract',
      ...metadata
    }
  });

  await fetch(upload.uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': contentType },
    body: fs.readFileSync(filePath)
  });

  // 2. Ingest (OCR + embedding generation)
  await client.vault.ingest(vaultId, upload.objectId);

  // 3. Wait for indexing
  let obj = await client.vault.objects.retrieve(vaultId, upload.objectId);
  while (obj.ingestionStatus === 'processing') {
    await new Promise(r => setTimeout(r, 5000));
    obj = await client.vault.objects.retrieve(vaultId, upload.objectId);
  }

  console.log(`Uploaded and indexed: ${filename} (${obj.ingestionStatus})`);
  return { objectId: upload.objectId, filename };
}
Ingestion handles everything. Vault ingestion automatically runs OCR on scanned PDFs, chunks the text, and generates embeddings. You don’t need to call OCR separately.

Step 3: Extract Key Terms

Use vault search to retrieve the contract text and extract structured terms:
async function extractKeyTerms(vaultId: string, objectId: string) {
  // Search for key sections of this specific contract
  const results = await client.vault.search(vaultId, {
    query: 'parties effective date termination payment obligations liability governing law',
    method: 'hybrid',
    limit: 15
  });

  const contractText = results.chunks.map(c => c.text).join('\n\n');

  const extraction = await client.llm.v1.chat.createCompletion({
    model: 'anthropic/claude-sonnet-4.5',
    messages: [
      {
        role: 'system',
        content: `You are a contract analyst. Extract key terms and return as JSON:
{
  "parties": [{"name": "...", "role": "..."}],
  "effective_date": "YYYY-MM-DD",
  "termination_date": "YYYY-MM-DD",
  "value": {"amount": 0, "currency": "USD"},
  "governing_law": "...",
  "key_obligations": ["..."],
  "termination_clauses": ["..."],
  "renewal": {"type": "auto|manual|none", "notice_period": "..."},
  "risk_flags": [{"clause": "...", "section": "...", "severity": "high|medium|low", "reason": "..."}]
}`
      },
      { role: 'user', content: contractText }
    ],
    temperature: 0
  });

  return JSON.parse(extraction.choices[0].message.content);
}
Example Output
{
  "parties": [
    {"name": "Acme Corp", "role": "Vendor"},
    {"name": "BigCo Inc", "role": "Client"}
  ],
  "effective_date": "2024-01-01",
  "termination_date": "2026-12-31",
  "value": {"amount": 500000, "currency": "USD"},
  "governing_law": "Delaware",
  "key_obligations": [
    "Vendor shall deliver software by Q2 2024",
    "Client shall provide access to systems within 30 days"
  ],
  "termination_clauses": [
    "Either party may terminate with 90 days notice",
    "Immediate termination for material breach"
  ],
  "renewal": {"type": "auto", "notice_period": "60 days"},
  "risk_flags": [
    {
      "clause": "Unlimited liability",
      "section": "Section 8.2",
      "severity": "high",
      "reason": "No cap on liability exposes vendor to unlimited damages"
    },
    {
      "clause": "Non-compete — 5 years",
      "section": "Section 12.1",
      "severity": "high",
      "reason": "Non-compete extends 5 years post-termination, unusually long"
    },
    {
      "clause": "Auto-renewal with price escalation",
      "section": "Section 3.4",
      "severity": "medium",
      "reason": "Annual 8% price increase on auto-renewal without cap"
    }
  ]
}

Step 4: Compare Clauses Across Contracts

Search your vault to find and compare similar clauses across multiple contracts:
async function compareClauses(vaultId: string, clauseType: string) {
  // Search for a specific clause type across all contracts
  const results = await client.vault.search(vaultId, {
    query: `${clauseType} clause terms conditions`,
    method: 'hybrid',
    limit: 20
  });

  const chunks = results.chunks.map(c => ({
    text: c.text,
    source: c.filename
  }));

  const comparison = await client.llm.v1.chat.createCompletion({
    model: 'anthropic/claude-sonnet-4.5',
    messages: [
      {
        role: 'system',
        content: `You are a senior contract attorney. Compare ${clauseType} clauses across multiple contracts.

For each contract:
- Summarize the clause terms
- Note any unusual or favorable/unfavorable provisions
- Rate the risk level (high/medium/low)

Then provide:
- A comparison table
- A recommendation on which terms are most favorable
- Suggested negotiation points for future contracts`
      },
      {
        role: 'user',
        content: `Compare these ${clauseType} clauses:\n\n${
          chunks.map(c => `### ${c.source}\n${c.text}`).join('\n\n')
        }`
      }
    ],
    temperature: 0.3
  });

  return comparison.choices[0].message.content;
}

// Example: Compare liability clauses across all contracts
const liabilityComparison = await compareClauses(vault.id, 'liability');
console.log(liabilityComparison);

Step 5: Generate a Risk Report

Combine all analysis into a formatted PDF report:
async function generateRiskReport(
  vaultId: string,
  contracts: Array<{ objectId: string; filename: string }>
) {
  let reportContent = `# Contract Risk Report\n\n**Generated:** ${new Date().toLocaleDateString()}\n**Contracts analyzed:** ${contracts.length}\n\n---\n\n`;

  // Analyze each contract
  for (const contract of contracts) {
    reportContent += `## ${contract.filename}\n\n`;

    const terms = await extractKeyTerms(vaultId, contract.objectId);

    reportContent += `**Parties:** ${terms.parties.map(p => `${p.name} (${p.role})`).join(', ')}\n`;
    reportContent += `**Effective:** ${terms.effective_date} — ${terms.termination_date}\n`;
    reportContent += `**Value:** ${terms.value.currency} ${terms.value.amount.toLocaleString()}\n`;
    reportContent += `**Governing Law:** ${terms.governing_law}\n\n`;

    if (terms.risk_flags.length > 0) {
      reportContent += `### Risk Flags\n\n`;
      reportContent += `| Severity | Clause | Section | Reason |\n|----------|--------|---------|--------|\n`;
      for (const flag of terms.risk_flags) {
        reportContent += `| **${flag.severity.toUpperCase()}** | ${flag.clause} | ${flag.section} | ${flag.reason} |\n`;
      }
      reportContent += '\n';
    }

    reportContent += '---\n\n';
  }

  // Cross-contract comparison
  reportContent += `## Cross-Contract Clause Comparison\n\n`;

  for (const clauseType of ['liability', 'termination', 'indemnification']) {
    reportContent += `### ${clauseType.charAt(0).toUpperCase() + clauseType.slice(1)} Clauses\n\n`;
    const comparison = await compareClauses(vaultId, clauseType);
    reportContent += comparison + '\n\n';
  }

  // Convert to PDF
  const report = await client.format.v1.document({
    content: reportContent,
    input_format: 'md',
    output_format: 'pdf',
    options: {
      header: 'CONFIDENTIAL — Contract Risk Report',
      footer: 'Page {{page}} of {{pages}}',
      styles: {
        h1: { font: 'Times New Roman', size: 16, bold: true, alignment: 'center' },
        h2: { font: 'Times New Roman', size: 13, bold: true },
        h3: { font: 'Times New Roman', size: 11, bold: true },
        p: { font: 'Times New Roman', size: 10, spacingAfter: 6 }
      }
    }
  });

  return report;
}

Production Tips

Error Handling

try {
  const terms = await extractKeyTerms(vaultId, objectId);
} catch (error) {
  if (error.status === 404) {
    console.error('Contract not found in vault');
  } else if (error.status === 429) {
    console.error('Rate limited — retry after a delay');
  } else {
    console.error('Extraction failed:', error.message);
  }
}

Use Webhooks for Production Pipelines

Instead of polling for ingestion status, subscribe to vault events:
await client.vault.events.subscriptions.create(vaultId, {
  url: 'https://your-app.com/webhooks/vault',
  events: ['object.ingested', 'object.failed']
});
Use temperature: 0 for key term extraction and risk identification. The lower temperature ensures more deterministic, factual outputs.

Next Steps