# GPAgent API v1

Programmatic access to your fund data. Every endpoint returns JSON and is authenticated with a bearer API key. GPAgent is the source of truth — any client (including [OpenClaw](https://github.com/anthropics/claude-code)) should read and write through this API rather than keep a local mirror.

- **Base URL** (production): `https://www.gpagent.ai/api/v1`
- **Base URL** (local dev): `http://localhost:3000/api/v1`
- **Auth header**: `Authorization: Bearer gpa_...`
- **Content type**: `application/json`

Create and manage keys at `/app/settings/api` in the web UI. Keys are per-user and can be revoked at any time.

> **Use the `www.` host.** The apex `gpagent.ai` issues a `307` redirect to `www.gpagent.ai`, and most HTTP clients (curl, fetch, reqwest) drop the `Authorization` header on cross-host redirects as a security measure. Pointing at `www.gpagent.ai` directly avoids an unauthenticated retry and the resulting `401`.

---

## Conventions

### Identifiers

Portfolio companies are addressed by a URL-safe `slug` (e.g. `phases`, `anthum-ai`). If you don't supply one on create, the API derives it from `name`. Slugs are unique per vehicle and stable across renames.

### Vehicle scoping

Every endpoint operates on a single vehicle (fund or SPV). If your API key has access to exactly one vehicle, it's selected automatically. If you have multiple, pass `?vehicleId=...` on every request — the API returns `400` with the list of accessible vehicle IDs otherwise.

### Soft delete

`DELETE` marks a record as deleted by setting `deletedAt`. Deleted records are excluded from list and detail reads. Re-creating via `PUT` with the same slug clears `deletedAt` and restores the record.

### Idempotency

| Verb | Behavior |
| --- | --- |
| `POST /portfolio` | Strict create. Returns `409` if the slug already exists. |
| `PUT /portfolio/{slug}` | Upsert. Safe to retry. Replaces the full record at that slug. |
| `PATCH /portfolio/{slug}` | Partial update. `404` if the slug does not exist. |
| `DELETE /portfolio/{slug}` | Soft delete. Safe to retry (idempotent). |

For sync use-cases, prefer `PUT` — it's the only write verb safe to retry after a network failure without worrying about duplicate state.

### Errors

All errors return JSON with at least an `error` field:

```json
{ "error": "Invalid or revoked API key" }
```

Validation errors include a zod `issues` array so clients can point at the offending field.

| Status | Meaning |
| --- | --- |
| `400` | Invalid JSON, failed validation, or ambiguous vehicle scope |
| `401` | Missing, malformed, or revoked API key |
| `403` | Key is valid but does not have access to the requested resource |
| `404` | Resource not found (or soft-deleted) |
| `409` | Slug conflict on `POST /portfolio` |

---

## Endpoints

### `GET /portfolio`

List the active fund vehicle, fund model, and all portfolio companies.

```bash
curl -H "Authorization: Bearer $GPAGENT_API_KEY" \
  https://www.gpagent.ai/api/v1/portfolio
```

**Response `200`**

```json
{
  "vehicle": {
    "id": "cmx...",
    "name": "Element 14 Capital Fund I",
    "type": "FUND",
    "slug": "fund-i"
  },
  "fund": {
    "targetFundSize": 1050000,
    "targetCompanyCount": 15,
    "maxConcentrationPct": 25,
    "totalDeployed": 275000,
    "deploymentPct": 26.2,
    "avgCheckSize": 27500,
    "avgValuation": 26100000,
    "companyCount": 10
  },
  "companies": [
    {
      "id": "cmx...",
      "slug": "phases",
      "name": "Phases",
      "website": "https://phases.so",
      "sector": "Developer Tools",
      "stage": "pre-seed",
      "checkSize": 25000,
      "postMoneyValuation": 20000000,
      "investmentDate": "2025-11-26T00:00:00.000Z",
      "instrumentType": "SAFE",
      "status": "ACTIVE",
      "notes": "YC S25. AI clinical trial recruitment.",
      "logoUrl": null,
      "createdAt": "2025-11-26T19:00:00.000Z",
      "updatedAt": "2026-03-12T10:00:00.000Z"
    }
  ],
  "sectors": [
    { "sector": "AI/ML", "count": 3, "deployed": 85000 }
  ]
}
```

---

### `POST /portfolio`

Create a new portfolio company. Strict create — if a company with the same slug already exists, returns `409`. Use `PUT /portfolio/{slug}` instead for safe-retry upserts.

**Request body** (JSON)

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `name` | string | yes | Display name |
| `slug` | string | no | Lowercase alphanumeric + hyphens. Derived from `name` if omitted. |
| `website` | string (URL) | no | |
| `sector` | string | no | |
| `stage` | string | no | e.g. `pre-seed`, `seed`, `series-a` |
| `checkSize` | number | yes | USD, non-negative |
| `postMoneyValuation` | number | no | USD, non-negative |
| `investmentDate` | string (ISO 8601) | yes | |
| `instrumentType` | enum | yes | `SAFE` \| `EQUITY` \| `CONVERTIBLE` |
| `status` | enum | no | `ACTIVE` (default) \| `MARKED_UP` \| `WRITTEN_DOWN` \| `EXITED` |
| `notes` | string | no | |
| `logoUrl` | string (URL) | no | |

```bash
curl -X POST \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Phases",
    "website": "https://phases.so",
    "sector": "Developer Tools",
    "stage": "pre-seed",
    "checkSize": 25000,
    "postMoneyValuation": 20000000,
    "investmentDate": "2025-11-26",
    "instrumentType": "SAFE",
    "notes": "YC S25."
  }' \
  https://www.gpagent.ai/api/v1/portfolio
```

**Response `201`** — the created company (same shape as `GET /portfolio/{slug}`).

---

### `GET /portfolio/{slug}`

Fetch a single portfolio company by slug.

```bash
curl -H "Authorization: Bearer $GPAGENT_API_KEY" \
  https://www.gpagent.ai/api/v1/portfolio/phases
```

**Response `200`**

```json
{
  "id": "cmx...",
  "slug": "phases",
  "name": "Phases",
  "website": "https://phases.so",
  "sector": "Developer Tools",
  "stage": "pre-seed",
  "checkSize": 25000,
  "postMoneyValuation": 20000000,
  "investmentDate": "2025-11-26T00:00:00.000Z",
  "instrumentType": "SAFE",
  "status": "ACTIVE",
  "notes": "YC S25.",
  "logoUrl": null,
  "createdAt": "2025-11-26T19:00:00.000Z",
  "updatedAt": "2026-03-12T10:00:00.000Z"
}
```

---

### `PUT /portfolio/{slug}`

Upsert a portfolio company by slug. Full-record replace. Safe to retry — this is the recommended write verb for sync clients.

Body shape matches `POST /portfolio`. `slug` in the body, if present, must match the URL slug.

```bash
curl -X PUT \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Phases",
    "website": "https://phases.so",
    "sector": "Developer Tools",
    "stage": "pre-seed",
    "checkSize": 25000,
    "postMoneyValuation": 20000000,
    "investmentDate": "2025-11-26",
    "instrumentType": "SAFE",
    "status": "MARKED_UP",
    "notes": "Marked up after their seed round."
  }' \
  https://www.gpagent.ai/api/v1/portfolio/phases
```

**Response `200`** on update, `201` on create. Body is the resulting company.

---

### `PATCH /portfolio/{slug}`

Partial update. Only the fields you send are changed; everything else is left alone. Returns `404` if the slug does not exist.

```bash
curl -X PATCH \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "MARKED_UP", "notes": "Seed round announced."}' \
  https://www.gpagent.ai/api/v1/portfolio/phases
```

**Response `200`** — the updated company.

---

### `DELETE /portfolio/{slug}`

Soft-delete a portfolio company. The record is retained in the database with `deletedAt` set and excluded from future reads. A subsequent `PUT /portfolio/{slug}` will restore it.

```bash
curl -X DELETE \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  https://www.gpagent.ai/api/v1/portfolio/phases
```

**Response `204`** (no body).

---

## Using GPAgent as a backing store for an agent

If you're wiring this API into an agent (OpenClaw, Claude Code, a custom script), the practical rules are:

1. **Never cache.** Query the API at the moment you need the data. GPAgent is cheap and always current.
2. **Use `PUT` for writes.** It's the only verb safe to retry after a timeout.
3. **Pipe the curl output through `jq`** if you want to let the agent read specific fields without parsing the whole response.
4. **Persist the API key outside the agent's working directory.** For OpenClaw, it belongs in `~/.openclaw/openclaw.json` under `env.GPAGENT_API_KEY`.
5. **Add this file to your agent's context.** The doc is served at `/api-docs` — point your agent's extra-context list at it (for OpenClaw: `agents.defaults.memorySearch.extraPaths`).

---

---

## Deal Pipeline

Deals are org-scoped (not vehicle-scoped) since a pipeline spans all funds. Vehicle auth is still required to identify your organization — the resolved `organizationId` is used.

### `GET /deals`

List all active deals in your pipeline. Supports filtering, pagination.

**Query parameters**

| Param | Type | Notes |
| --- | --- | --- |
| `source` | enum | `YC` \| `ANGELLIST` \| `REFERRAL` \| `INBOUND` \| `MANUAL` |
| `stage` | enum | `DISCOVERED` \| `RESEARCHING` \| `PRE_MEETING` \| `MEETING` \| `TERMS` \| `COMMITTED` \| `PASSED` |
| `batch` | string | Filter by `sourceDetail` (case-insensitive contains). E.g. `batch=W26` |
| `limit` | number | Max records to return. Default `50`, max `200`. |
| `offset` | number | Pagination offset. Default `0`. |

```bash
# All RESEARCHING deals from YC W26
curl -H "Authorization: Bearer $GPAGENT_API_KEY" \
  "https://www.gpagent.ai/api/v1/deals?stage=RESEARCHING&batch=W26"
```

**Response `200`**

```json
{
  "total": 12,
  "limit": 50,
  "offset": 0,
  "deals": [
    {
      "id": "cma...",
      "slug": "raindrop",
      "companyName": "Raindrop",
      "website": "https://raindrop.dev",
      "description": "Sentry for AI agents.",
      "source": "YC",
      "sourceDetail": "YC W26",
      "stage": "RESEARCHING",
      "thesisFitScore": 5,
      "estimatedValuation": 20000000,
      "estimatedCheckSize": null,
      "sector": "AI/ML",
      "companyStage": "seed",
      "notes": "...",
      "logoUrl": null,
      "links": [],
      "scoutedByAgentId": "cma...",
      "createdAt": "2026-03-22T10:00:00.000Z",
      "updatedAt": "2026-03-22T10:00:00.000Z"
    }
  ]
}
```

---

### `POST /deals`

Create a new deal. Strict create — returns `409` if a deal with the same slug already exists for this org.

**Request body** (JSON)

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `companyName` | string | yes | |
| `slug` | string | no | Derived from `companyName` if omitted. |
| `website` | string (URL) | no | |
| `description` | string | no | |
| `source` | enum | no | Default `MANUAL` |
| `sourceDetail` | string | no | e.g. `YC W26` |
| `stage` | enum | no | Default `DISCOVERED`. One of `DISCOVERED`, `RESEARCHING`, `PRE_MEETING`, `MEETING`, `TERMS`, `COMMITTED`, `PASSED`. |
| `thesisFitScore` | number (0–10) | no | |
| `estimatedValuation` | number | no | USD |
| `estimatedCheckSize` | number | no | USD |
| `sector` | string | no | |
| `companyStage` | string | no | e.g. `pre-seed`, `seed` |
| `notes` | string | no | |
| `logoUrl` | string (URL) | no | |
| `links` | array | no | `[{ type, label, url }]` |
| `scoutedByAgentId` | string | no | Agent that sourced this deal |

**Response `201`** — the created deal.

---

### `GET /deals/{slug}`

Fetch a single deal by slug. Returns the deal plus all comments.

```bash
curl -H "Authorization: Bearer $GPAGENT_API_KEY" \
  https://www.gpagent.ai/api/v1/deals/raindrop
```

**Response `200`** — deal object with `comments` array appended:

```json
{
  "id": "cma...",
  "slug": "raindrop",
  "companyName": "Raindrop",
  "...",
  "comments": [
    {
      "id": "cmc...",
      "dealId": "cma...",
      "authorName": "Scout",
      "authorType": "agent",
      "content": "High thesis fit. Prioritize at Demo Day.",
      "metadata": {},
      "createdAt": "2026-03-22T10:05:00.000Z"
    }
  ]
}
```

---

### `PUT /deals/{slug}`

Upsert a deal by slug. Full-record replace. Safe to retry.

Body shape matches `POST /deals`. `slug` in the body, if present, must match the URL slug.

**Response `200`** on update, `201` on create.

---

### `PATCH /deals/{slug}`

Partial update. Send only the fields you want to change. Useful for stage transitions.

```bash
# Advance a deal to MEETING
curl -X PATCH \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stage": "MEETING"}' \
  https://www.gpagent.ai/api/v1/deals/raindrop
```

**Response `200`** — the updated deal. `404` if slug not found.

---

### `DELETE /deals/{slug}`

Soft-delete a deal. Sets `deletedAt`; excluded from future reads. A subsequent `PUT` restores it.

**Response `204`** (no body).

---

### `POST /deals/{slug}/comments`

Add a comment to a deal.

**Request body** (JSON)

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `authorName` | string | yes | Display name |
| `authorType` | enum | no | `human` (default) \| `agent` \| `system` |
| `content` | string | yes | |
| `metadata` | object | no | Arbitrary JSON. Default `{}` |

```bash
curl -X POST \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"authorName": "Scout", "authorType": "agent", "content": "Confirmed round is open."}' \
  https://www.gpagent.ai/api/v1/deals/raindrop/comments
```

**Response `201`** — the created comment.

---

## Scoring Criteria

Organization-level scoring rubric used by the Scout agent to evaluate deals.

### `GET /scoring-criteria`

Return the current scoring criteria for your organization.

```bash
curl -H "Authorization: Bearer $GPAGENT_API_KEY" \
  https://www.gpagent.ai/api/v1/scoring-criteria
```

**Response `200`**

```json
{
  "organizationId": "cma...",
  "organizationName": "Element 14 Capital",
  "criteria": {
    "domainExpertise": {
      "4": "Founder/market fit. Complementary team...",
      "3": "Some domain knowledge...",
      "2": "Limited background...",
      "1": "No relevant background..."
    },
    "product": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "fundraisability": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "market": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "execution": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "traction": { "4": "...", "3": "...", "2": "...", "1": "..." }
  }
}
```

If no custom criteria are set, returns the E14 Capital defaults.

---

### `PUT /scoring-criteria`

Full replace of the scoring criteria. All 6 categories and all 4 score levels are required.

```bash
curl -X PUT \
  -H "Authorization: Bearer $GPAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domainExpertise": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "product": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "fundraisability": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "market": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "execution": { "4": "...", "3": "...", "2": "...", "1": "..." },
    "traction": { "4": "...", "3": "...", "2": "...", "1": "..." }
  }' \
  https://www.gpagent.ai/api/v1/scoring-criteria
```

**Response `200`** — the saved criteria (same shape as GET).

---

## Changelog

- **2026-04-16** — Added `PRE_MEETING` to `DealStage` enum (sits between `RESEARCHING` and `MEETING`); agents own `DISCOVERED`/`RESEARCHING`, GPs own `PRE_MEETING` onward.
- **2026-04-16** — `GET/POST /deals`, `GET/PUT/PATCH/DELETE /deals/{slug}`, `POST /deals/{slug}/comments`, `GET/PUT /scoring-criteria`.
- **2026-04-10** — v1 launch: `GET/POST /portfolio`, `GET/PUT/PATCH/DELETE /portfolio/{slug}`. Deal pipeline and Updates endpoints shipping next.
