# CompCode — agent guide

You are driving the CompCode API on behalf of a RevOps operator. CompCode is a "commissions as code" platform: commission plans, quotas, assignments, statements, and recalcs are all exposed via REST. Your job is to translate the operator's intent into the right API calls without breaking anything.

Read this entire file before you make your first call. The mental-model and hard-rules sections will save you (and the operator) hours.

---

## Connection

- **Base URL:** `https://app.compcode.ai`
- **Auth header:** `Authorization: Bearer ${COMPCODE_API_KEY}` — workspace keys start with `ws_`
- **Machine-readable spec:** `https://app.compcode.ai/api/openapi.json` — fetch this when you need a schema detail that's not in this file. It is the source of truth for every endpoint.
- **Human docs:** `https://compcode.ai/docs`
- **Interactive reference:** `https://app.compcode.ai/api-docs`
- **Error envelope:** `{ "statusCode": 4xx, "code": "string_slug", "message": "...", "details"?: {...} }` — match on `code`, never on the message string.

If the operator hasn't set `COMPCODE_API_KEY`, ask them once. Tell them they can generate a workspace key at `https://app.compcode.ai/settings/api-keys`.

---

## Plan IDs

Plans have a stable identity (`planId`) and a versioned config (`planVersionId`). Use `planId` for everything in `/api/assignments`, `/api/quotas`, `/api/commissions/*`. Use `planVersionId` only when you need to target a specific historical config — version status changes, per-rep tier overrides. Every plan response carries both.

---

## Happy-path recipes

These are the five workflows operators actually want to do end-to-end. Each is a curl chain you can run as-is, substituting real IDs from the prior response.

### 1. Author a plan, assign reps, set quotas, simulate, recalc

You're an LLM — generate the `planConfig` yourself. The schema lives in `/api/openapi.json` under the `POST /api/plans` request body (`config` property). Common building blocks: one or more `rules`, each with `tiers[]` keyed by `minThreshold` and either `rate` or `flatAmount`, plus optional `dealConditions[]`.

```bash
# 1. Create the plan
curl -s -X POST https://app.compcode.ai/api/plans \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "AE Plan 2026", "effectiveStart": "2026-01-01", "config": <planConfig you built> }'
# → { plan: { planVersionId, planId, rules: [{ id, name, ... }] } }
# Capture: planId (for steps 3–6) and rules[].id (for quotas)

# 2. List reps to find IDs
curl -s "https://app.compcode.ai/api/reps" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"
# → [ { id, email, name, role, active }, ... ]

# 3. Assign reps to the plan
curl -s -X POST https://app.compcode.ai/api/assignments \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "repIds": ["<rep_id>"], "planId": "<planId>", "effectiveStart": "2026-01-01" }'

# 4. Set quotas (one call per rule; use planRuleId from step 1)
curl -s -X POST https://app.compcode.ai/api/quotas \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "repIds": ["<rep_id>"], "planRuleId": "<rule_id>", "period": "2026-Q1", "target": 500000, "variableTarget": 25000 }'

# 5. Dry-run simulation BEFORE writing events
curl -s -X POST https://app.compcode.ai/api/commissions/simulate \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "<planId>", "period": "2026-Q1" }'
# → per-rule breakdown with engine trace. Review before step 6.

# 6. Recalculate for real (writes commission events)
curl -s -X POST https://app.compcode.ai/api/commissions/recalculate \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "<planId>" }'
# → { triggered, count, scope }
```

### 2. Onboard a rep mid-period

```bash
# Confirm rep exists (search by email — never create a rep blind)
curl -s "https://app.compcode.ai/api/reps?email=new.hire@acme.com" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"

# Assign to the plan
curl -s -X POST https://app.compcode.ai/api/assignments \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "repIds": ["<rep_id>"], "planId": "<planId>", "effectiveStart": "2026-05-15" }'

# Set quota (prorated by the operator's policy)
curl -s -X POST https://app.compcode.ai/api/quotas \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "repIds": ["<rep_id>"], "planRuleId": "<rule_id>", "period": "2026-Q2", "target": 250000 }'

# Recalculate for the new rep's deals
curl -s -X POST https://app.compcode.ai/api/commissions/recalculate \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "repId": "<rep_id>" }'
```

### 3. Close the month

```bash
# COM-188: workspace-level batch generate — one call for every active rep.
# Omit `repIds` to target every active rep; pass it to scope the run.
curl -s -X POST https://app.compcode.ai/api/statements/generate \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "period": "2026-04" }'
# → { period, generated, skipped, errors,
#     statements: [{repId, version, statementId, netPayout}, ...],
#     skippedReps: [{repId, reason: "already_approved"}, ...],
#     errorReps:   [{repId, error}, ...] }

# Or, for one rep at a time (same shape, single statement back):
curl -s -X POST https://app.compcode.ai/api/statements/<rep_id>/generate \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "period": "2026-04" }'
# → { generated, statementId, version, period, repId, status: "draft",
#     summary: { totalCommission, totalBonuses, totalAdjustments, netPayout },
#     dealCount, lineItems: [...] }

# Review with the operator. Once approved (per-rep):
# NOTE: the body MUST be `{period, status:"approved"}`. There is no
# `{approve: true}` shorthand — guessing it returns 400 VALIDATION_FAILED.
# Same shape unlocks: `{period, status:"draft"}`.
curl -s -X POST https://app.compcode.ai/api/statements/<rep_id>/approve \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "period": "2026-04", "status": "approved" }'

# Export for payroll
curl -s "https://app.compcode.ai/api/statements/<rep_id>/export?period=2026-04&format=csv" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"
```

### 4. Adjust after approval

Approved statements are locked. To change them:

```bash
# 1. Unlock by reverting to draft
curl -s -X POST https://app.compcode.ai/api/statements/<rep_id>/approve \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "period": "2026-04", "status": "draft" }'

# 2. Either fix upstream (plan/quota) + recalc, OR post a manual adjustment
curl -s -X POST https://app.compcode.ai/api/statements/<rep_id>/adjust \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 500, "reason": "Spiff for new-logo deal", "period": "2026-04" }'

# 3. Regenerate + re-approve
curl -s -X POST https://app.compcode.ai/api/statements/<rep_id>/generate \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -d '{ "period": "2026-04" }'
curl -s -X POST https://app.compcode.ai/api/statements/<rep_id>/approve \
  -H "Authorization: Bearer $COMPCODE_API_KEY" \
  -d '{ "period": "2026-04", "status": "approved" }'
```

For partial-period reconciliation (e.g. retroactive tier change for one rep), use `POST /api/statements/<rep_id>/true-up` instead of full unlock/recalc.

### 5. Diagnose "why isn't my commission showing up?"

Walk these in order. Stop at the first answer.

```bash
# 1. Did the orchestrator run for this deal?
curl -s "https://app.compcode.ai/api/audit-log?actor=orchestrator&dealId=<deal_id>" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"
# Look at outcome + counters: eventsWritten, eventsReversed, statementLocked, skipped.

# 2. Did any commission events actually get written?
curl -s "https://app.compcode.ai/api/commissions?dealId=<deal_id>" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"

# 3. Is the rep assigned to a plan with effective dates covering the deal's close date?
curl -s "https://app.compcode.ai/api/assignments?repId=<rep_id>" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"

# 4. Does the rep have a quota for the period? (no quota → 0% attainment → likely no tier match)
curl -s "https://app.compcode.ai/api/quotas?repId=<rep_id>&period=2026-Q1" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"

# 5. Confirm the deal snapshot has the field the plan measures
curl -s "https://app.compcode.ai/api/deals/<deal_id>" \
  -H "Authorization: Bearer $COMPCODE_API_KEY"
```

If everything looks right but events are missing, re-run a single-deal recalc: `POST /api/commissions/recalculate` with `{ "dealId": "<deal_id>" }`.

---

## Mental model

These are the invariants the engine assumes. Violating them produces wrong numbers, not error messages.

- **Idempotency.** Commission events are deduplicated by `idempotency_key`. Re-running recalc on the same deal won't duplicate events. Safe to retry.
- **Current-state events (R9).** Each commission event represents the **latest** state of a deal under a rule, not an audit-trail entry. When a deal changes (stage flips, amount edits, deletion), the engine mutates the event in place and increments `eventsReversed`. History lives in `commission_event_history`.
- **Statement lock.** An `approved` statement blocks **all** downstream commission writes for that (rep, period) — both webhooks AND manual recalcs. Engine response will show `blocked.statementLocked: true`. To unblock, set status back to `draft` via `POST /api/statements/:repId/approve {period, status:"draft"}`.
- **Rep matching.** Reps are uniquely identified by **email AND CRM member ID** together. Always search via `GET /api/reps?email=...` before assuming a rep doesn't exist. Creating one blind produces ghost rows; commissions land on the wrong rep.
- **Quota attainment.** Lives on an atomic counter at `quotas.current_value`, not aggregated from events at read time. If a quota is missing or has the wrong period format, attainment is 0 and tiers won't fire.
- **Period strings.** Format must match the rule's `attainmentPeriod` exactly:
  - `monthly` → `YYYY-MM` (e.g. `2026-01`)
  - `quarterly` → `YYYY-QN` (e.g. `2026-Q1`)
  - `annual` → `YYYY` (e.g. `2026`)
  - Mismatched format silently falls back to default — quota goes unfound.
- **Field aliases.** `stage`/`dealstage` and `amount`/`value` are auto-aliased in plan conditions. But `deal_snapshots` stores **normalized** names, so if you're inspecting raw deal data, use the CRM's native slug (`hs_deal_amount`, `value`, etc.). Get the full list from `GET /api/fields/attributes`.
- **Cap/floor semantics.** `capMultiplier` is a multiple of variable target (e.g. `2.0` = max 2× variable). `floorAttainment` is an attainment gate (e.g. `0.5` = no payout below 50%). The legacy `cap` and `floor` fields store **flat dollars** and are deprecated — never use them.
- **CRM sync delay.** After a webhook fires, GETs against the same deal can be stale for ~200ms (Postgres pooler caching). Retry once after a short delay before reporting a missing event.

---

## Hard rules (never break)

1. **Simulate before recalculating.** Bad commission events are sticky — cleanup needs SQL access the operator doesn't have. Always run `POST /api/commissions/simulate` first and show the operator the per-rule breakdown.
2. **Pass `planId` (identity) to `/api/assignments`, `/api/quotas`, `/api/commissions/*`.** `planVersionId` only goes to version-targeted endpoints (status changes, overrides).
3. **Prefer `planRuleId` over the `measure` shortcut** when posting quotas. `measure` only resolves cleanly when the rule name is unique across the rep's assigned plans.
4. **Use `capMultiplier` / `floorAttainment`**, never legacy `cap` / `floor` — those store flat dollars and will silently destroy payouts.
5. **Read errors from `code`, not `message` or `error`.** Error envelope is `{ statusCode, code, message, details? }`.
6. **Don't parallelize recalc within a single (rep, period) group.** Marginal-tier progression assumes sequential ordering. Parallelize across reps or across periods, never within.
7. **Search before creating reps.** Always `GET /api/reps?email=...` before any rep mutation.
8. **Retry GET-after-write by ~200ms** if the read looks stale. The Neon pooler occasionally serves stale snapshots.
9. **Statement approve/revert body is `{period, status}`**. `status: "approved"` locks, `status: "draft"` unlocks. There is no `approve: true` shorthand — that returns `400 VALIDATION_FAILED` with message `period and status required`.

---

## Error recovery

| Error code                  | What it means                                | What to do                                                                                          |
|-----------------------------|----------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `plan_rule_not_found`       | `planRuleId` doesn't exist on this plan/ver  | Re-fetch the plan and use `rules[].id` from the response — your reference is probably from a prior plan version that's been superseded. |
| `statement_locked`          | (Web)hook blocked by approved statement      | `POST /api/statements/:repId/approve` with `{period, status:"draft"}`, then retry.                  |
| `rep_not_found`             | repId doesn't resolve                        | `GET /api/reps?email=...` first; ask the operator which rep is canonical.                           |
| `duplicate_idempotency_key` | Event already exists                         | Safe to ignore — the prior write succeeded.                                                         |
| `period_mismatch`           | Period format ≠ rule's `attainmentPeriod`    | Use `YYYY-MM` for monthly, `YYYY-QN` for quarterly, `YYYY` for annual.                              |
| `quota_ambiguous_measure`   | `measure` name matches rules on >1 plan      | Re-post with explicit `planRuleId`.                                                                 |
| `forbidden`                 | Role check failed                            | Plans/quotas/recalc/assignments require **admin** role. Statements: **manager** or **admin**.       |

For anything else, fetch `https://app.compcode.ai/api/openapi.json` and look up the endpoint's documented `ApiResponse` list.

---

## When to surface vs. just do

- **Always confirm before:** recalc with no `dealId`/`repId`/`planId` scope (= whole workspace), unlocking an approved statement, deleting a plan or assignment, posting an adjustment.
- **Just do (no confirmation needed):** simulate, list/get endpoints, generating a statement snapshot (it's reversible — generation creates a draft).
- **Always show the operator** the engine trace from a simulate response before the real recalc. The trace is the audit story.

---

## Discovery

- Full endpoint list: `GET /api/openapi.json` (61 endpoints, OpenAPI 3.0).
- Available deal fields in the connected CRM: `GET /api/fields/attributes`.
- Audit trail of every engine run: `GET /api/audit-log?actor=orchestrator`.
- Workspace info, CRM connection state, billing: `GET /api/workspace`.
