Skip to main content

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:

TrackWhat you'll build
Track 1 — REST AppA Node.js service that manages equity via the REST API
Track 2 — MCP AgentA Claude-powered cap table agent using the MCP server
Track 3 — Full StackA Next.js app with AINative SSO and live cap table UI

Prerequisites


Track 1 — REST API App

Build a Node.js service that automates equity management.

1. Get a token

// 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

// 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

// 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

// 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

npm install -g @opencapstack/mcp-server

Add to ~/.claude.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)

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

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

Create .env.local:

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

// 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

// 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

// 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

// 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