What You’ll Build
A contract analysis pipeline that:
- Uploads contracts to a searchable vault
- Extracts key terms (parties, dates, amounts, obligations)
- Identifies risky clauses with severity ratings
- Compares similar clauses across multiple contracts
- 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.
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);
}
{
"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