A parametric legal drafting and analysis tool that transforms indemnification clauses into an interactive, visual, and educational experience.
Indemnification Architect helps you:
-
Draft indemnification clauses by adjusting visual sliders that represent every component of a clause (obligation type, triggers, scope, caps, etc.). Each position generates production-quality legal text.
-
Analyze existing clauses by pasting text and having AI map it to slider positions, instantly visualizing the risk balance.
The tool surfaces risk alerts when your selected positions create problematic combinations — like uncapped indemnification with broad triggers that could bypass liability caps.
- 22 Adjustable Components: Obligation type, triggers, scope, caps, notice, defense control, IP coverage, and more
- Real-time Clause Generation: Watch the legal text update as you adjust sliders
- Risk Signal Alerts: Visual warnings when combinations create escape hatches or gaps
- Multi-Provider AI Support: Works with Anthropic Claude, OpenAI GPT, Google Gemini, Groq, OpenRouter, or local Ollama
- Secure API Key Storage: Browser-encrypted key storage option (no server transmission)
- Dark-Themed UI: Professional legal drafting interface
Adjust each component along a spectrum from "Favors Party A" to "Favors Party B":
The tool alerts you when slider combinations create problematic escape hatches or gaps:
Choose your preferred installation method:
| Method | Best For | Requirements |
|---|---|---|
| Docker Desktop | Easy setup, no Node.js needed | Docker Desktop |
| Local Development | Development, customization | Node.js 18+ |
You'll also need an LLM provider — choose one:
- Anthropic API key (Claude)
- OpenAI API key (GPT)
- Google AI API key (Gemini)
- Groq API key
- OpenRouter API key
- Ollama (free, runs locally)
The easiest way to run Indemnification Architect. No Node.js installation required.
- Docker Desktop — Download here
git clone https://github.com/Tucuxi-Inc/IndemnificationArchitect.git
cd IndemnificationArchitect/indemnification-architectCreate a .env file with your API keys:
cp .env.example .env
# Edit .env with your API key(s)Or skip this step and configure keys in the app's Settings dialog later.
docker-compose up -d --buildNavigate to http://localhost:3004
# Start the container
docker-compose up -d
# Stop the container
docker-compose down
# View logs
docker-compose logs -f
# Rebuild after code changes
docker-compose up -d --buildIf Ollama runs on your host machine, the container connects to it automatically via host.docker.internal:
# Ensure Ollama is running on your host
ollama serve
# Start the Docker container
docker-compose up -d
# In the app Settings, select "Ollama" as the providerFor development or if you prefer running without Docker.
- Node.js 18+ — Download here
- npm (comes with Node.js)
git clone https://github.com/Tucuxi-Inc/IndemnificationArchitect.git
cd IndemnificationArchitect/indemnification-architectnpm installCreate a .env file:
cp .env.example .envAdd your API key(s):
# For Anthropic (Claude)
ANTHROPIC_API_KEY=sk-ant-your-key-here
# For OpenAI (GPT)
OPENAI_API_KEY=sk-your-key-here
# For Google (Gemini)
GOOGLE_API_KEY=your-key-here
# For Groq
GROQ_API_KEY=gsk_your-key-here
# For OpenRouter
OPENROUTER_API_KEY=sk-or-your-key-here
# For Ollama (local) - no API key needed
OLLAMA_BASE_URL=http://localhost:11434Or configure keys in the app's Settings dialog after starting.
npm run devNavigate to http://localhost:3000
-
Adjust Sliders: Each slider represents a component of the indemnification clause. Positions range from "Favors Party A" (left) to "Favors Party B" (right).
-
Enable/Disable Components: Use the checkbox next to each component to include or exclude it from the clause.
-
Watch the Clause Update: The right panel shows the generated clause text in real-time.
-
Check Risk Signals: The Risk Signal Panel shows warnings when your positions create problematic combinations.
-
Export: Use the export controls to copy the clause to your clipboard.
-
Paste a Clause: Click the "Paste or Upload Clause" section and paste your existing indemnification clause text.
-
Click Analyze: The AI will parse the clause and map it to slider positions.
-
Review the Mapping: Sliders will be set to match your clause. Components not found will be disabled.
-
Modify as Needed: Adjust sliders to change positions or enable/disable components to add/remove provisions.
-
Click the Settings button in the header.
-
Select a Provider: Choose from Anthropic, OpenAI, Google, Groq, OpenRouter, or Ollama.
-
Select a Model: Choose the specific model to use.
-
Enter API Key (for cloud providers): Enter your API key and click Save.
-
Test Connection: Click "Test Connection" to verify it works.
Ollama lets you run AI models on your own computer — no API key or internet required.
-
Download Ollama from ollama.com
-
Install and run Ollama
-
Download a model (in your terminal):
# Good general-purpose model (~4GB) ollama pull llama3.2 # Smaller, faster model (~2GB) ollama pull phi3 # Larger, more capable model (~8GB) ollama pull llama3.3
-
Start Ollama (it should auto-start, but if not):
ollama serve
-
Configure in the app:
- Open Settings
- Select "Ollama" as the provider
- Select your installed model
- The base URL defaults to
http://localhost:11434 - Click Test Connection
Note: Local models may be slower than cloud APIs for complex clause analysis. Consider using a cloud provider for faster results.
This section explains the complete technical architecture of how the application calculates risk, generates clauses, and parses existing indemnification text.
The application decomposes an indemnification clause into 22 adjustable components (15 primary + 7 IP sub-components). Each component is defined in src/data/components.json with the following structure:
{
id: string, // Unique identifier (e.g., "obligation", "triggers")
name: string, // Display name
weight: number, // 0-1, contribution to overall balance
description: string, // Explanation of the component
category: string, // "core" | "procedure" | "limitation" | "supporting" | "ip"
parentId: string | null, // null for primary, "ip" for IP sub-components
defaultPosition: number, // Starting position (typically center)
positions: Position[] // Array of 3-8 discrete positions
}Each position within a component represents a specific legal posture:
{
index: number, // 0 = most Party A favorable, max = most Party B favorable
label: string, // Full description (e.g., "Defend, indemnify, and hold harmless")
shortLabel: string, // Abbreviated label for UI
clauseFragment: string, // Actual legal text to insert into clause
partyAImpact: string, // Plain-language explanation of impact on Party A
partyBImpact: string, // Plain-language explanation of impact on Party B
escapeHatchRisk: "green" | "yellow" | "red",
escapeHatchNote: string, // Explanation of escape hatch risk
interactionIds: string[] // IDs of cross-component interactions this triggers
}| Component | Weight | Category | Positions | Description |
|---|---|---|---|---|
| obligation | 0.07 | core | 5 | Defend/indemnify/hold harmless duty |
| triggers | 0.12 | core | 7 | Events that activate indemnification |
| scope | 0.11 | core | 6 | Types of losses covered |
| directionality | 0.035 | core | 5 | Which party indemnifies which |
| notice | 0.05 | procedure | 6 | Notice requirements for claims |
| defenseControl | 0.05 | procedure | 5 | Who controls the defense |
| settlementConsent | 0.045 | procedure | 5 | Consent requirements for settlements |
| cap | 0.16 | limitation | 6 | Financial limitation on indemnification |
| survival | 0.06 | limitation | 5 | How long indemnification survives |
| exclusivity | 0.04 | limitation | 3 | Whether this is the exclusive remedy |
| coveredPersons | 0.025 | supporting | 5 | Who can claim indemnification |
| contributoryFault | 0.035 | supporting | 4 | Effect of indemnitee's own fault |
| mitigate | 0.02 | supporting | 4 | Duty to mitigate damages |
| insurance | 0.025 | supporting | 4 | Insurance requirements |
| subrogation | 0.015 | supporting | 4 | Subrogation rights |
| ip.rightsCovered | 0.05 | ip | 8 | Which IP rights are covered |
| ip.infringementStandard | 0.02 | ip | 5 | Standard for infringement claims |
| ip.geoScope | 0.015 | ip | 5 | Geographic scope of IP coverage |
| ip.exclusions | 0.025 | ip | 5 | Carve-outs from IP indemnification |
| ip.remediation | 0.025 | ip | 6 | Remediation options for IP claims |
| ip.damages | 0.02 | ip | 3 | Types of IP damages covered |
| ip.openSource | 0.025 | ip | 5 | Open source compliance coverage |
Total weights sum to exactly 1.00 — this is critical for the master position calculation.
The master slider displays a 0-100 score representing the overall balance of the clause:
- 0 = Strongly favors Party A (indemnitee)
- 50 = Balanced
- 100 = Strongly favors Party B (indemnitor)
Master Position = Σ (normalizedPosition_i × effectiveWeight_i)
where:
normalizedPosition_i = (position_i / maxPosition_i) × 100
effectiveWeight_i = weight_i / Σ(weights of enabled components)
-
Normalize each component position to 0-100:
- If a component has 5 positions (0-4), position 2 normalizes to:
(2/4) × 100 = 50
- If a component has 5 positions (0-4), position 2 normalizes to:
-
Calculate effective weights based on enabled components:
- If "obligation" (weight 0.07) and "triggers" (weight 0.12) are the only enabled components:
- Total enabled weight = 0.19
- Effective weight of obligation = 0.07 / 0.19 ≈ 0.368
- Effective weight of triggers = 0.12 / 0.19 ≈ 0.632
-
Compute weighted sum:
masterPosition = (normalizedObligation × 0.368) + (normalizedTriggers × 0.632)
-
Round to 2 decimal places for display
When a component is disabled, its weight is redistributed proportionally to all remaining enabled components. This ensures:
- Disabled components don't affect the master position
- The master position always reflects only the active components
- Total effective weight always equals 1.0
| Score Range | Classification |
|---|---|
| 0-29 | Strongly Favors Party A |
| 30-44 | Moderately Favors Party A |
| 45-55 | Balanced |
| 56-70 | Moderately Favors Party B |
| 71-100 | Strongly Favors Party B |
When the user drags the master slider, all enabled sub-sliders adjust proportionally:
1. Calculate delta = targetMasterPos - currentMasterPos
2. For each enabled component:
- Calculate availableMovement:
- If moving up (delta > 0): 100 - normalizedPosition
- If moving down (delta < 0): normalizedPosition
- Calculate weightedAvailable = availableMovement × effectiveWeight
3. Distribute delta proportionally:
- share_i = weightedAvailable_i / Σ(all weightedAvailable)
- componentDelta_i = delta × share_i
4. Apply new positions (clamped to valid range)
This ensures components with more "room to move" absorb more of the change, while respecting each component's minimum and maximum positions.
The application monitors 18 documented interactions that detect when combinations of component positions create risk conditions. Interactions are defined in src/data/interactions.json:
{
id: string, // Unique identifier
title: string, // Short description
severity: "green" | "yellow" | "red",
componentIds: string[], // Components involved
triggerCondition: {
type: "AND" | "OR",
conditions: [
{ componentId: string, operator: "<=" | ">=" | "==" | "!=", value: number }
]
},
explanation: string, // Why this is risky
recommendation: string // How to fix it
}1. Liability Cap Bypass (RED severity)
Trigger: triggers.position <= 2 AND cap.position == 0
Risk: Breach-based triggers combined with uncapped indemnification create
an end-run around the agreement's limitation of liability.
Recommendation: Narrow triggers to fault-based (position ≥3) OR include
indemnification within the liability cap (position ≥3).
2. Consequential Damages Pathway (RED severity)
Trigger: scope.position == 0 AND cap.position <= 1
Risk: When scope includes consequential damages and there's no meaningful cap,
indemnification becomes the sole pathway to recover unlimited consequentials.
Recommendation: Either narrow scope to exclude consequential damages OR
set cap at position ≥3.
3. Open Source Conflict (RED severity)
Trigger: ip.openSource.position >= 3 AND ip.rightsCovered.position <= 2
Risk: Excluding open source from IP indemnification while including copyrights
creates an interpretive conflict for OSS-derived code.
Recommendation: Either include OSS explicitly (position ≤2) OR narrow
copyright coverage to exclude OSS-derived works.
4. Remedy Gap (YELLOW severity)
Trigger: exclusivity.position >= 2 AND triggers.position >= 3
Risk: Exclusive remedy combined with narrow triggers leaves claims
falling through a remedy gap.
Recommendation: Broaden triggers (position ≤2) OR make remedy non-exclusive.
For each interaction:
For each condition in triggerCondition.conditions:
- Get component state
- If component disabled: condition = false
- Else: evaluate position against operator and value
If type == "AND": all conditions must be true
If type == "OR": any condition must be true
If triggered: add to active interactions
Sort by severity (red → yellow → green)
Return sorted list
When component positions change, the clause assembly engine (src/engine/assembler.ts) generates legal text in real-time.
-
Load clause templates from
src/data/clauseTemplates.json -
For each section template:
- Check if required components are enabled
- If not, skip section
-
For each subsection:
- Resolve
{{componentId}}placeholders with clause fragments - Track which defined terms are used
- Resolve
-
Build structured output:
{ sections: [ { id: string, title: string, sectionNumber: number, subsections: [ { id, title, text, sourceComponents[] } ] } ], definedTerms: [{ term, definition }], rawText: string, // Plain text version metadata: { positions: Record<componentId, { enabled, position }>, timestamp: number } }
{
"id": "general_main",
"template": "{{directionality}} {{obligation}} the Indemnified Party and {{coveredPersons}} from and against {{scope}} arising out of or relating to {{triggers}}.",
"sourceComponents": ["directionality", "obligation", "coveredPersons", "scope", "triggers"]
}If all referenced components are enabled, the template resolves to a complete sentence using each component's clauseFragment for its current position.
When a user pastes an existing indemnification clause, the application uses an LLM to parse it and map it to component positions.
User pastes clause
↓
ClauseInput component captures text
↓
POST /api/parse with:
- clauseText (the pasted clause)
- provider (e.g., "anthropic")
- model (e.g., "claude-sonnet-4-20250514")
- apiKey (optional, from browser storage)
↓
Server builds prompt with full component taxonomy (~8-10KB)
↓
LLM analyzes clause against taxonomy
↓
LLM returns JSON mapping:
{
components: {
[componentId]: {
enabled: boolean, // Is this component present?
position: number, // Which position matches best?
confidence: number, // 0.0-1.0 confidence score
excerpt: string, // Exact text from clause
reasoning: string // Why this position was chosen
}
},
overallBalance: number, // 0-100 estimated balance
warnings: string[], // Parsing ambiguities
unmappedText: string[] // Text that didn't map to any component
}
↓
Server validates response (positions within range, confidence 0-1)
↓
Client receives result, calls loadParsedResult()
↓
Zustand store updates componentStates
↓
UI re-renders with sliders at parsed positions
You are a legal analysis assistant specializing in indemnification clauses.
Your task is to analyze an indemnification clause and map its provisions to
a standardized component taxonomy.
For each component, determine:
1. enabled: Is this component present in the clause? (true/false)
2. position: Which position (0 to max) best matches the clause language?
3. confidence: How confident are you in this mapping? (0.0 to 1.0)
4. excerpt: The specific text from the clause that maps to this component
5. reasoning: Brief explanation of why you chose this position
IMPORTANT:
- Only mark a component as enabled if it is explicitly addressed in the clause
- Choose the position that most closely matches the clause language
- If unsure between positions, choose the one that errs toward Party A (lower position)
- Set confidence lower if the language is ambiguous
Return your analysis as a valid JSON object...
The full prompt includes the complete taxonomy of all 22 components with all their positions, impacts, and descriptions.
The application uses Zustand for state management with two stores:
{
mode: "drafting" | "analysis",
componentStates: Record<componentId, { enabled: boolean, position: number }>,
originalClause: string | null,
parsedResult: ParsedClauseResult | null,
smoothedClause: string | null,
smoothingInProgress: boolean,
parsingInProgress: boolean,
// Actions
setComponentPosition(id, position),
setComponentEnabled(id, enabled),
setMasterPosition(position),
loadParsedResult(result),
resetToDefaults()
}{
llmProvider: "anthropic" | "openai" | "google" | "groq" | "openrouter" | "ollama",
llmModel: string,
ollamaBaseUrl: string,
apiKeyStatus: Record<provider, boolean>,
_hasHydrated: boolean, // Tracks localStorage hydration
// Actions
setProvider(provider),
setModel(model),
setApiKey(provider, key),
getApiKey(provider)
}The store exposes computed values via selector hooks:
useMasterPosition() // Computed 0-100 balance score
useActiveInteractions() // Array of triggered interactions
useAssembledClause() // Generated clause object
useEffectiveWeights() // Weight distribution after redistributionAPI keys stored in the browser use Web Crypto API encryption:
- Key derivation: PBKDF2 with SHA-256, 100,000 iterations
- Encryption: AES-GCM with random IV
- Storage: Base64-encoded ciphertext in localStorage
- Transmission: Keys sent in request body only when needed (HTTPS)
Browser-stored keys never leave the device in plaintext and are never logged server-side.
indemnification-architect/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── page.tsx # Main application page
│ │ ├── layout.tsx # Root layout with fonts
│ │ ├── globals.css # Global styles
│ │ └── api/
│ │ ├── parse/route.ts # Clause parsing endpoint
│ │ ├── smooth/route.ts # Clause smoothing endpoint
│ │ └── settings/route.ts # Provider status endpoint
│ ├── components/
│ │ ├── MasterSlider.tsx # Overall balance slider
│ │ ├── ComponentGroup.tsx # Accordion group of sliders
│ │ ├── ComponentSlider.tsx # Individual component slider
│ │ ├── IPSection.tsx # IP-specific components
│ │ ├── ClauseDisplay.tsx # Generated clause panel
│ │ ├── RiskSignalPanel.tsx # Active interactions display
│ │ ├── ClauseInput.tsx # Paste/upload for analysis
│ │ ├── SettingsDialog.tsx # LLM configuration
│ │ └── ui/ # Shared UI primitives
│ ├── data/
│ │ ├── components.json # 22 component definitions
│ │ ├── interactions.json # 18 interaction definitions
│ │ ├── clauseTemplates.json # Clause assembly templates
│ │ └── presets.json # Contract type presets
│ ├── engine/
│ │ ├── calculator.ts # Master position math
│ │ ├── interactions.ts # Interaction detection
│ │ └── assembler.ts # Clause assembly
│ ├── store/
│ │ ├── useStore.ts # Main application state
│ │ └── useSettingsStore.ts # LLM settings state
│ ├── lib/
│ │ ├── secureStorage.ts # Encrypted key storage
│ │ ├── export.ts # Export utilities
│ │ └── llm/ # LLM client implementations
│ │ ├── index.ts # Client factory
│ │ ├── types.ts # LLMClient interface
│ │ ├── anthropic.ts # Claude client
│ │ ├── openai.ts # GPT client
│ │ ├── google.ts # Gemini client
│ │ ├── groq.ts # Groq client
│ │ ├── openrouter.ts # OpenRouter client
│ │ └── ollama.ts # Ollama client
│ └── types/
│ └── index.ts # TypeScript definitions
├── docs/
│ ├── screenshots/ # UI screenshots
│ ├── KNOWLEDGE_BASE.md # Complete component taxonomy
│ ├── DEVELOPMENT_PLAN.md # Build phases
│ ├── INTERACTION_MATRIX.md # Interaction details
│ └── HANDOFF.md # Developer handoff doc
├── CLAUDE.md # AI development context
├── README.md # This file
├── LICENSE # MIT License
└── package.json
- Check your API key: Make sure it's entered correctly (no extra spaces)
- Check your internet connection: Cloud providers require internet access
- For Ollama: Make sure Ollama is running (
ollama serve)
- Local models (Ollama) can be slow on complex clauses
- Try a smaller/faster model
- Use the Cancel button and try a cloud provider
- Simplify the clause text
- Make sure your
.envfile exists in the project root - Make sure the key is correctly formatted
- Restart the dev server after changing
.env - Or enter the key in the Settings dialog instead
# Delete node_modules and reinstall
rm -rf node_modules
npm install# Use a different port
npm run dev -- -p 3001This tool generates draft language for informational and educational purposes. Output should be reviewed by qualified legal counsel.
This tool does not provide legal advice and does not create an attorney-client relationship. All generated clauses are drafts that should be reviewed and modified by a licensed attorney before use in any legal document.
- Next.js 14 — React framework
- TypeScript — Type safety
- Tailwind CSS — Styling
- Zustand — State management
- Framer Motion — Animations
- Radix UI — Accessible components
| Provider | Best For | Speed |
|---|---|---|
| Anthropic (Claude) | High-quality analysis | Fast |
| OpenAI (GPT-4o) | General purpose | Fast |
| Google (Gemini) | Fast responses | Very Fast |
| Groq | Speed with open models | Very Fast |
| OpenRouter | Provider flexibility | Varies |
| Ollama | Privacy, offline use | Slower |
- API keys can be stored encrypted in the browser using Web Crypto API
- Browser-stored keys never leave your device in plaintext
- Server-side .env keys are never exposed to the client
- No user data is stored on any server
Contributions are welcome! Please read the CLAUDE.md file for development context and the docs/HANDOFF.md for implementation details.
MIT License — See LICENSE file for details.
© 2026 Tucuxi, Inc.





