LegalOSS130ktracked
Legal AI & NLP

IndemnificationArchitect

Tucuxi-Inc/IndemnificationArchitect

Indemnification Architect

A parametric legal drafting and analysis tool that transforms indemnification clauses into an interactive, visual, and educational experience.

License Next.js TypeScript

Main Interface

What is This?

Indemnification Architect helps you:

  1. 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.

  2. 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.

Features

  • 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

Component Sliders

Adjust each component along a spectrum from "Favors Party A" to "Favors Party B":

Component Sliders

Risk Signal Detection

The tool alerts you when slider combinations create problematic escape hatches or gaps:

Risk Signals

Quick Start

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:


Option A: Docker Desktop (Recommended)

The easiest way to run Indemnification Architect. No Node.js installation required.

Prerequisites

1. Clone the Repository

git clone https://github.com/Tucuxi-Inc/IndemnificationArchitect.git
cd IndemnificationArchitect/indemnification-architect

2. Configure API Keys (Optional)

Create 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.

3. Build and Run

docker-compose up -d --build

4. Open the App

Navigate to http://localhost:3004

Docker Commands Reference

# 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 --build

Using Ollama with Docker

If 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 provider

Option B: Local Development

For development or if you prefer running without Docker.

Prerequisites

1. Clone the Repository

git clone https://github.com/Tucuxi-Inc/IndemnificationArchitect.git
cd IndemnificationArchitect/indemnification-architect

2. Install Dependencies

npm install

3. Configure API Keys (Optional)

Create a .env file:

cp .env.example .env

Add 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:11434

Or configure keys in the app's Settings dialog after starting.

4. Start the Application

npm run dev

5. Open the App

Navigate to http://localhost:3000


Using the Application

Drafting Mode

  1. Adjust Sliders: Each slider represents a component of the indemnification clause. Positions range from "Favors Party A" (left) to "Favors Party B" (right).

  2. Enable/Disable Components: Use the checkbox next to each component to include or exclude it from the clause.

  3. Watch the Clause Update: The right panel shows the generated clause text in real-time.

  4. Check Risk Signals: The Risk Signal Panel shows warnings when your positions create problematic combinations.

  5. Export: Use the export controls to copy the clause to your clipboard.

Generated Clause Output

Analysis Mode

  1. Paste a Clause: Click the "Paste or Upload Clause" section and paste your existing indemnification clause text.

  2. Click Analyze: The AI will parse the clause and map it to slider positions.

  3. Review the Mapping: Sliders will be set to match your clause. Components not found will be disabled.

  4. Modify as Needed: Adjust sliders to change positions or enable/disable components to add/remove provisions.

Analysis Mode

Configuring LLM Providers

  1. Click the Settings button in the header.

  2. Select a Provider: Choose from Anthropic, OpenAI, Google, Groq, OpenRouter, or Ollama.

  3. Select a Model: Choose the specific model to use.

  4. Enter API Key (for cloud providers): Enter your API key and click Save.

  5. Test Connection: Click "Test Connection" to verify it works.

Settings Dialog

Using Ollama (Free, Local AI)

Ollama lets you run AI models on your own computer — no API key or internet required.

Installing Ollama

  1. Download Ollama from ollama.com

  2. Install and run Ollama

  3. 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
  4. Start Ollama (it should auto-start, but if not):

    ollama serve
  5. 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.


How It Works: Technical Deep Dive

This section explains the complete technical architecture of how the application calculates risk, generates clauses, and parses existing indemnification text.

Component Data Model

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
}

The 22 Components and Their Weights

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.

Master Position Calculation (Risk Balance Score)

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)

Formula

Master Position = Σ (normalizedPosition_i × effectiveWeight_i)

where:
  normalizedPosition_i = (position_i / maxPosition_i) × 100
  effectiveWeight_i = weight_i / Σ(weights of enabled components)

Step-by-Step Calculation

  1. Normalize each component position to 0-100:

    • If a component has 5 positions (0-4), position 2 normalizes to: (2/4) × 100 = 50
  2. 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
  3. Compute weighted sum:

    • masterPosition = (normalizedObligation × 0.368) + (normalizedTriggers × 0.632)
  4. Round to 2 decimal places for display

Weight Redistribution

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

Balance Classification

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

Bidirectional Master Slider

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.

Cross-Component Interaction Detection

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
}

Example Interactions

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.

Detection Algorithm

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

Clause Assembly Engine

When component positions change, the clause assembly engine (src/engine/assembler.ts) generates legal text in real-time.

Assembly Process

  1. Load clause templates from src/data/clauseTemplates.json

  2. For each section template:

    • Check if required components are enabled
    • If not, skip section
  3. For each subsection:

    • Resolve {{componentId}} placeholders with clause fragments
    • Track which defined terms are used
  4. 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
      }
    }

Template Example

{
  "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.

Clause Parsing (Analysis Mode)

When a user pastes an existing indemnification clause, the application uses an LLM to parse it and map it to component positions.

Parsing Flow

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

LLM System Prompt (Excerpt)

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.

State Management

The application uses Zustand for state management with two stores:

Main Store (src/store/useStore.ts)

{
  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()
}

Settings Store (src/store/useSettingsStore.ts)

{
  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)
}

Derived Selectors

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 redistribution

API Key Security

API keys stored in the browser use Web Crypto API encryption:

  1. Key derivation: PBKDF2 with SHA-256, 100,000 iterations
  2. Encryption: AES-GCM with random IV
  3. Storage: Base64-encoded ciphertext in localStorage
  4. 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.


Project Structure

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

Troubleshooting

"Failed to connect to [provider]"

  • 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)

"Analysis is taking too long"

  • 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

"ANTHROPIC_API_KEY environment variable is not set"

  • Make sure your .env file 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

"Module not found" or other npm errors

# Delete node_modules and reinstall
rm -rf node_modules
npm install

Port 3000 is already in use

# Use a different port
npm run dev -- -p 3001

Legal Disclaimer

This 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.

Technical Stack

Built With

LLM Providers

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

Security

  • 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

Contributing

Contributions are welcome! Please read the CLAUDE.md file for development context and the docs/HANDOFF.md for implementation details.

License

MIT License — See LICENSE file for details.


© 2026 Tucuxi, Inc.