---
title: "Builders Guide"
description: "End-to-end guide for building on OpenCap Stack — from first API call to production agent"
canonical: "https://docs.ainative.studio/docs/opencapstack/builders-guide"
last-updated: "2026-10-03T21:47:10.764Z"
---

# Builders Guide

Source: https://docs.ainative.studio/docs/opencapstack/builders-guide

> End-to-end guide for building on OpenCap Stack — from first API call to production agent

# Builders Guide

This guide walks you through building a complete cap table application on top of OpenCap Stack — from authentication to a fully functional equity management agent. We cover three tracks:

| Track | What you'll build |
|-------|------------------|
| [Track 1 — REST App](#track-1--rest-api-app) | A Node.js service that manages equity via the REST API |
| [Track 2 — MCP Agent](#track-2--mcp-agent) | A Claude-powered cap table agent using the MCP server |
| [Track 3 — Full Stack](#track-3--full-stack-nextjs) | A Next.js app with AINative SSO and live cap table UI |

---

## Prerequisites

- OpenCap Stack account at [opencapstack.com/register](https://opencapstack.com/register), or a [self-hosted instance](./self-hosting)
- Node.js 18+ for REST/Next.js tracks
- An AINative account (optional — for SSO and ZeroDB memory)

---

## Track 1 — REST API App

Build a Node.js service that automates equity management.

### 1. Get a token

```javascript
// auth.js
const BASE_URL = 'https://api.opencapstack.com/api/v1';

export async function getToken() {
  const res = await fetch(`${BASE_URL}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: process.env.OPENCAP_EMAIL,
      password: process.env.OPENCAP_PASSWORD,
    }),
  });
  const { token } = await res.json();
  return token;
}

// Or use agent onboarding for server-to-server (no expiry):
export async function getAgentToken() {
  const res = await fetch(`${BASE_URL}/agents/onboard`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      agent_id: 'my-equity-service',
      capabilities: ['read:cap-table', 'write:equity', 'write:stakeholders'],
    }),
  });
  const { api_key } = await res.json();
  return api_key;
}
```

### 2. Create an API client

```javascript
// client.js
export class OpenCapClient {
  constructor(token) {
    this.base = 'https://api.opencapstack.com/api/v1';
    this.headers = {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    };
  }

  async get(path) {
    const res = await fetch(`${this.base}${path}`, { headers: this.headers });
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }

  async post(path, body) {
    const res = await fetch(`${this.base}${path}`, {
      method: 'POST',
      headers: this.headers,
      body: JSON.stringify(body),
    });
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }
}
```

### 3. Onboard a new employee

```javascript
// onboard-employee.js
import { OpenCapClient } from './client.js';
import { getAgentToken } from './auth.js';

async function onboardEmployee({ name, email, title, optionCount }) {
  const token = await getAgentToken();
  const client = new OpenCapClient(token);

  // 1. Create stakeholder
  const stakeholder = await client.post('/stakeholders', {
    name,
    email,
    type: 'EMPLOYEE',
    title,
  });

  // 2. Get the options pool share class
  const shareClasses = await client.get('/share-classes');
  const optionsPool = shareClasses.data.find(sc => sc.type === 'OPTIONS_POOL');

  // 3. Issue equity grant
  const grant = await client.post('/equity-grants', {
    stakeholderId: stakeholder.id,
    shareClassId: optionsPool.id,
    quantity: optionCount,
    grantType: 'OPTION',
    strikePrice: 0.10,
    vestingSchedule: '4_YEAR_1_YEAR_CLIFF',
    grantDate: new Date().toISOString().split('T')[0],
  });

  console.log(`✓ Onboarded ${name}: ${optionCount} options — Grant #${grant.grantNumber}`);
  return { stakeholder, grant };
}

// Run it
onboardEmployee({
  name: 'Alex Kim',
  email: 'alex@startup.com',
  title: 'Senior Engineer',
  optionCount: 50000,
});
```

### 4. Build a cap table report

```javascript
// report.js
async function generateCapTableReport(client) {
  const [summary, valuations] = await Promise.all([
    client.get('/cap-table'),
    client.get('/valuations/latest'),
  ]);

  const { totalIssuedShares, shareClasses, topStakeholders } = summary;
  const fmv = valuations?.pricePerShare ?? 'N/A';

  console.log(`\n═══ Cap Table as of ${new Date().toLocaleDateString()} ═══`);
  console.log(`FMV per share: $${fmv}`);
  console.log(`Total issued: ${totalIssuedShares.toLocaleString()} shares\n`);

  for (const sh of topStakeholders) {
    const pct = (sh.ownership * 100).toFixed(1);
    console.log(`  ${sh.name.padEnd(30)} ${sh.shares.toLocaleString().padStart(12)} shares  ${pct}%`);
  }
}
```

---

## Track 2 — MCP Agent

Build a Claude-powered equity agent using the OpenCap Stack MCP server.

### 1. Install and configure

```bash
npm install -g @opencapstack/mcp-server
```

Add to `~/.claude.json`:
```json
{
  "mcpServers": {
    "opencap": {
      "command": "npx",
      "args": ["-y", "@opencapstack/mcp-server"],
      "env": {
        "OPENCAP_API_KEY": "ocs_live_your_key_here",
        "OPENCAP_BASE_URL": "https://api.opencapstack.com/api/v1"
      }
    }
  }
}
```

### 2. Write a system prompt for your agent

If you're building a product with an embedded Claude agent, give it a focused system prompt:

```
You are EquityBot, an AI assistant for cap table management built on OpenCap Stack.

You have access to the following MCP tools:
- cap_table_summary, get_fully_diluted_shares, calculate_dilution, run_waterfall_analysis
- list_stakeholders, create_stakeholder, update_stakeholder
- list_equity_grants, create_equity_grant, get_equity_grant
- list_safes, create_safe, get_safe
- get_latest_valuation, create_financial_report

Rules:
- Always confirm destructive actions (delete, approve grants, create SAFEs) before executing
- Show ownership percentages when listing stakeholders
- Format share counts with commas (e.g. 1,000,000)
- When creating grants, always confirm the vesting schedule with the user
- Never share individual stakeholder email addresses unless explicitly asked
```

### 3. Use the Anthropic SDK with MCP (programmatic)

```javascript
import Anthropic from '@anthropic-ai/sdk';
import { spawn } from 'child_process';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

async function createEquityAgent() {
  // Start the MCP server
  const transport = new StdioClientTransport({
    command: 'npx',
    args: ['-y', '@opencapstack/mcp-server'],
    env: {
      ...process.env,
      OPENCAP_API_KEY: process.env.OPENCAP_API_KEY,
      OPENCAP_BASE_URL: 'https://api.opencapstack.com/api/v1',
    },
  });

  const mcp = new Client({ name: 'equity-agent', version: '1.0.0' }, { capabilities: {} });
  await mcp.connect(transport);

  // Get available tools
  const { tools } = await mcp.listTools();
  const claudeTools = tools.map(t => ({
    name: t.name,
    description: t.description,
    input_schema: t.inputSchema,
  }));

  const claude = new Anthropic();

  async function chat(userMessage, history = []) {
    const messages = [...history, { role: 'user', content: userMessage }];

    let response = await claude.messages.create({
      model: 'claude-sonnet-4-6',
      max_tokens: 4096,
      tools: claudeTools,
      messages,
    });

    // Handle tool calls
    while (response.stop_reason === 'tool_use') {
      const toolUses = response.content.filter(b => b.type === 'tool_use');
      const toolResults = await Promise.all(
        toolUses.map(async tu => {
          const result = await mcp.callTool({ name: tu.name, arguments: tu.input });
          return {
            type: 'tool_result',
            tool_use_id: tu.id,
            content: JSON.stringify(result.content[0]?.text ?? result),
          };
        })
      );

      messages.push({ role: 'assistant', content: response.content });
      messages.push({ role: 'user', content: toolResults });

      response = await claude.messages.create({
        model: 'claude-sonnet-4-6',
        max_tokens: 4096,
        tools: claudeTools,
        messages,
      });
    }

    return response.content[0].text;
  }

  return { chat, mcp };
}

// Usage
const agent = await createEquityAgent();
const answer = await agent.chat('What does our cap table look like?');
console.log(answer);
```

---

## Track 3 — Full Stack Next.js

Build a complete cap table dashboard with AINative SSO.

### 1. Set up the project

```bash
npx create-next-app@latest my-cap-table --typescript --tailwind --app
cd my-cap-table
npm install @opencapstack/mcp-server
```

Create `.env.local`:
```bash
OPENCAP_BASE_URL=https://api.opencapstack.com/api/v1
OPENCAP_ADMIN_SECRET=your-admin-secret   # only needed for server-side token generation
AINATIVE_API_URL=https://api.ainative.studio
```

### 2. Auth service

```typescript
// lib/auth.ts
const BASE = process.env.NEXT_PUBLIC_API_URL ?? '/api/v1';

export async function loginWithAINative(email: string, password: string) {
  const res = await fetch(`${BASE}/auth/ainative-login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error('Login failed');
  const { token, user } = await res.json();
  document.cookie = `session=${token}; path=/; secure; samesite=strict`;
  return { token, user };
}

export async function getSession(): Promise<string | null> {
  const match = document.cookie.match(/session=([^;]+)/);
  return match ? match[1] : null;
}
```

### 3. Cap table data hook

```typescript
// hooks/useCapTable.ts
import { useState, useEffect } from 'react';
import { getSession } from '@/lib/auth';

export function useCapTable() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function load() {
      const token = await getSession();
      const res = await fetch('/api/v1/cap-table', {
        headers: { Authorization: `Bearer ${token}` },
      });
      setData(await res.json());
      setLoading(false);
    }
    load();
  }, []);

  return { data, loading };
}
```

### 4. Cap table dashboard page

```tsx
// app/dashboard/cap-table/page.tsx
'use client';
import { useCapTable } from '@/hooks/useCapTable';

export default function CapTablePage() {
  const { data, loading } = useCapTable();

  if (loading) return <div>Loading cap table...</div>;

  return (
    <div className="p-8">
      <h1 className="text-2xl font-bold mb-6">Cap Table</h1>

      <div className="grid grid-cols-3 gap-4 mb-8">
        <StatCard label="Total Issued" value={data.totalIssuedShares.toLocaleString()} />
        <StatCard label="Fully Diluted" value={data.fullyDilutedShares.toLocaleString()} />
        <StatCard label="Share Classes" value={data.shareClasses.length} />
      </div>

      <table className="w-full text-sm">
        <thead>
          <tr className="border-b text-left text-gray-500">
            <th className="pb-2">Stakeholder</th>
            <th className="pb-2 text-right">Shares</th>
            <th className="pb-2 text-right">Ownership</th>
          </tr>
        </thead>
        <tbody>
          {data.topStakeholders.map(sh => (
            <tr key={sh.name} className="border-b py-2">
              <td className="py-3">{sh.name}</td>
              <td className="py-3 text-right">{sh.shares.toLocaleString()}</td>
              <td className="py-3 text-right">{(sh.ownership * 100).toFixed(1)}%</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function StatCard({ label, value }) {
  return (
    <div className="border rounded-lg p-4">
      <p className="text-sm text-gray-500">{label}</p>
      <p className="text-2xl font-bold mt-1">{value}</p>
    </div>
  );
}
```

### 5. Protected routes via Next.js middleware

```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PUBLIC = ['/', '/login', '/register', '/pricing', '/developers'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  if (PUBLIC.some(p => pathname.startsWith(p))) return NextResponse.next();

  const token = request.cookies.get('session')?.value;
  if (token) return NextResponse.next();

  return NextResponse.redirect(new URL(`/login?redirect=${pathname}`, request.url));
}
```

---

## Production Checklist

Before going live, verify:

- [ ] Agent API key stored in environment variables, not in code
- [ ] `OPENCAP_BASE_URL` includes `/api/v1`
- [ ] Token refresh logic handles 401 responses automatically
- [ ] Rate limit headers monitored — alert if `X-RateLimit-Remaining` drops below 10%
- [ ] Destructive operations (delete stakeholder, approve grant) require user confirmation
- [ ] Documents uploaded via multipart/form-data, not base64 JSON
- [ ] OCTA export tested if you need interoperability with other cap table tools

---

## Next Steps

- [OCTA Schema](./octa-schema) — understand the data model and interoperability
- [MCP Server tools reference](./mcp-server) — full list of 29 tools
- [Self-Hosting](./self-hosting) — run your own OpenCap Stack instance
- [REST API Reference](./rest-api) — complete endpoint documentation
