Three ~28s tracks Karti generated, re-encoded from 160kbps to 96kbps and pulled from -15 LUFS to -26. They were mastered at foreground level; the failure mode of background music in a tool someone has open for eight hours is not "too quiet". Playback volume is a further 0.14 on top. AUTOPLAY DOES NOT MEAN AUTOPLAY. Every current browser refuses audio with sound until the page has had a real user gesture — Chrome sometimes relents for a site with a high Media Engagement Index, Safari essentially never does on a first visit. So `play()` rejects on mount, and the naive version of this looks like a bug: the control says playing and nothing is audible. This tries immediately, and on refusal arms a one-shot listener and starts on the first click or keypress. Verified in Chrome: paused on load, playing 2.2s after the first click. That also happens to be the kind behaviour for someone who opened six tabs at once. Mounted in Shell, NOT in App. The anonymous Learn page renders outside Shell, and a share-code visitor opening a link someone sent them should not get unexpected audio — that is the one context where it reads as a fault rather than as character. Asserted by there being no <audio> element on that page at all. Preference is localStorage and deliberately not mirrored to the server, on the same reasoning as the sidebar: whether you want music depends on whether you are wearing headphones, not on who you are. Also pauses on tab hide, because music from a tab nobody is looking at is the thing people hunt through twenty tabs to kill. The element is rendered rather than `new Audio()` so it is inspectable in devtools and in a test; `preload="none"` means a user who mutes it never downloads a track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🐷 PIG — Prime Intellect Growth
An open-source, agent-native CRM for two-sided AI-compute companies.
Self-hostable. Auditable. Built for teams that buy GPU capacity on one side and sell it on the other.
Why this exists
A company that aggregates GPU capacity and resells it does not run one pipeline. It runs two, and its business is the spread between them.
Today that spread is usually managed in a spreadsheet with a margin calculator in column K, a document of supplier terms, and a general-purpose CRM that has no idea what an H100-hour is. Salesforce, HubSpot and Attio model a single pipeline of deals against companies. They have no concept of inventory, no concept of a commitment you already bought and are paying for whether or not it sells, and therefore no way to answer the question the business turns on:
Which contracted capacity is sold, to whom, at what margin — and what is idle right now?
PIG is one ledger that knows the domain. The load-bearing table is
allocations, which joins a
capacity_commitment (what you bought, at a known cost) to a demand_deal
(what you sold, at a known price). Margin, utilisation and idle capacity all
fall out of that one join. Everything else is plumbing that keeps the ledger
honest.
Cost is charged against the full commitment, not only the hours that sold. Unsold hours are already paid for. Charging only the allocated share reports a healthy margin on a block that is losing money, which is precisely the failure PIG exists to prevent. There is a test pinning it.
Who it is for
PIG models three teams, because two-sided compute companies have three constituencies competing for the same scarce capacity.
| Team | Job to be done |
|---|---|
| Supply | Source, qualify, price and contract GPU capacity from providers |
| Demand | Sell compute and post-training; renew and expand accounts |
| Research | Consume capacity internally — real burn, no revenue |
Research is a first-class tenant rather than an afterthought: internal burn competes with revenue for the same GPUs, and margin arithmetic that cannot see it is wrong.
The team set is configurable in packages/core/src/ontology.ts. PIG ships with
these three because they match the structure of the company it was designed
for, not because they are universal.
Screenshots
Placeholder — fresh captures needed. The application shell was rebuilt as a three-pane layout (header, collapsible sidebar rail, docked Piggy) and every screenshot taken before that redesign now misrepresents the product. Rather than ship misleading images, this section is deliberately empty until the new shell is re-shot at desktop and at 393px, in light and dark.
Pages to capture:
/Overview,/margin,/capacity(the matcher),/calendar,/piggydocked beside a record.
The two-sided data model
Forty-seven tables, but the shape is small. These are the ones that carry the thesis:
| Table | What it holds | Why it is not in a generic CRM |
|---|---|---|
capacity_commitments |
What you bought: term, GPU-hours, cost per GPU-hour, floor and ceiling, and a shape ({intervals[], quantities[]}) |
Real contracts ramp across tranches and step down at checkpoints; a single start/end/total reports availability that does not exist in the month someone wants it |
demand_deals |
What you are selling: ACV, product line, MSA/DPA state, stage | The paper state is a separate axis from the stage, because paper gates delivery |
supply_deals |
The other pipeline: sourcing a provider through diligence to live | Generic CRMs have one pipeline and call the supplier a vendor |
allocations |
The join. Commitment × deal × GPU-hours × window × status | This is the whole product. Margin, utilisation and idle all derive from it |
inventory_listings |
Market availability mirrored from the Prime Intellect API | Sync is a straight field mapping, not an ETL project |
capacity_requests |
What a customer asked for, whether or not it could be served | Unservable demand is the signal for what to buy next |
contracts + sla_terms + sla_metric_targets + contract_obligations |
Polymorphic over party and type — MSA, DPA, SLA, order form, capacity commitment — with negotiated SLA terms and dated obligations | The supply side negotiates heavyweight paper; the self-serve demand side runs on a reliability tier and a credits policy instead |
export_authorizations, compliance_artifacts, compliance_decisions |
Export-control determinations recorded on the allocation edge, with reasoning and rule version | US controls apply an ultimate-parent test that reaches through the corporate tree, so country of incorporation is not a valid key |
facts |
Every agent-derived claim, with score, band, evidence excerpt and source URL | An agent allowed to write unattributed claims will eventually write a wrong one and nobody will be able to tell which |
agent_tasks / agent_runs / agent_actions |
The queue the API writes to and the agent drains, plus what it did | The API never calls the model; it writes a row |
Two pipelines, with stages taken from how the market operates:
Demand: qualification → legal → scoping → proposal → procurement
→ POC → deployment → expansion (+ closed_won / closed_lost)
Supply: sourced → qualifying → technical diligence → financial diligence
→ pricing → contracting → onboarding → live → renewal
(+ churned / rejected)
Legal sits second in the demand pipeline. MSA and DPA execution gates the deal rather than closing it. Most CRMs put contracts at the end of the funnel and are wrong about it for this market.
Three further decisions worth knowing before you read the schema:
- Holds reserve; they do not sell. A live hold removes capacity from everyone else's availability — otherwise two sellers promise the same GPUs — but never counts toward utilisation or revenue.
- Security tiers are ranked, not labelled.
community_cloud<secure_cloud<government, and a requirement is satisfied only from at or above its tier. - Money is integer cents, rounded exactly once, at the boundary.
Self-hosting
Requirements
Node 22+, pnpm 11+ (pinned by packageManager; corepack enable installs it),
and a PostgreSQL 16 database that PIG owns exclusively.
Development
corepack enable
pnpm install
docker run -d --name pig-dev -p 5432:5432 \
-e POSTGRES_USER=pig -e POSTGRES_PASSWORD=pig -e POSTGRES_DB=pig \
postgres:16-alpine
export DATABASE_URL=postgres://pig:pig@localhost:5432/pig
pnpm run db:migrate
pnpm run db:seed # optional — sourced, cited, confidence-graded people
pnpm run db:demo # optional — a plausible demo book, prefixed "DEMO — "
pnpm run dev:api # :8920
pnpm run dev:web # :5173, proxies /api to :8920
With no identity provider configured, authentication is disabled in
development and every request runs as the first user in the table.
loadConfig refuses to start with NODE_ENV=production in that state, so it
cannot leak into a deployment.
Production
cp .env.example .env # then edit
docker compose -p pig up -d db
docker compose -p pig run --rm --no-deps app pnpm exec tsx packages/db/src/migrate.ts
docker compose -p pig up -d --build app
Migrate from a one-off container before the app starts, not with exec: a
release that queries a table its migration has not yet created crash-loops
before you can attach to it. Full deployment notes, including the reverse
proxy, the release poller and rollback semantics, are in
deploy/README.md.
Every environment variable
Read from apps/api/src/lib/config.ts (API), apps/piggy/src/config.ts
(Piggy) and docker-compose.yml. Bold means no default.
Required
| Variable | Default | Notes |
|---|---|---|
DATABASE_URL |
— | The only unconditionally required value. PIG owns this database exclusively |
POSTGRES_PASSWORD |
— | Compose only; docker-compose.yml refuses to start without it |
In production you must additionally set either SUPABASE_URL or
PIG_OIDC_ISSUER. The API throws at boot with neither.
Identity
| Variable | Default | Notes |
|---|---|---|
SUPABASE_URL |
unset | Hosted path. Absent in development ⇒ auth disabled |
SUPABASE_ANON_KEY |
unset | Public by design; served to the browser via /api/config |
SUPABASE_SERVICE_KEY |
unset | Only for administrative provisioning and self-registration. Warns at boot when set |
PIG_OIDC_ISSUER |
unset | On-premises path. Takes precedence over SUPABASE_URL |
PIG_OIDC_JWKS_URI |
discovered | Set it to skip discovery on an air-gapped network |
PIG_OIDC_AUDIENCE |
unset | Strongly recommended: without it, any token your provider issued for any application in the same tenant is accepted here. Warns, does not refuse |
PIG_OIDC_EMAIL_CLAIMS |
provider defaults | Comma-separated, in preference order |
Server
| Variable | Default | Notes |
|---|---|---|
PIG_PORT |
8920 |
|
PIG_PUBLIC_URL |
http://localhost:8920 |
The single origin the app is served from; CORS and the Google redirect are validated against it |
NODE_ENV |
development |
production activates the identity-provider guard |
PIG_ADMIN_EMAILS |
'' |
Comma-separated. Every address must already have an account — an unregistered address here is a standing offer of admin rights to whoever claims it first |
PIG_INVITE_CODE |
unset | Set it to gate signup |
PIG_SETTINGS_ENCRYPTION_KEY |
unset | Base64-encoded 32 bytes. Required for Notion and Google OAuth; secrets written in the admin UI need it |
Prime Intellect
| Variable | Default | Notes |
|---|---|---|
PRIME_API_KEY |
unset | Scope it to Availability → Read only |
PRIME_API_BASE |
https://api.primeintellect.ai |
The compute/pods host. Inference is a different host — see below |
PRIME_SYNC_ENABLED |
false |
Warns if on without a key |
PRIME_SYNC_INTERVAL_MINUTES |
30 |
Piggy
The API and the Piggy container read overlapping but distinct sets.
| Variable | Default | Read by | Notes |
|---|---|---|---|
PIGGY_ENABLED |
false |
API | Gates the chat surface |
PIGGY_INFERENCE_API_KEY |
— | Piggy | Required by the Piggy process. The model credential never reaches the API container |
PIGGY_INFERENCE_BASE |
https://api.pinference.ai/api/v1 |
both | OpenAI-compatible |
PIGGY_MODEL |
nvidia/nemotron-3-nano-30b-a3b |
both | Admin-selectable at runtime too |
PIGGY_LEASE_SECONDS |
300 |
both | Queue lease duration |
PIGGY_POLL_INTERVAL_MS |
2000 |
Piggy | |
PIGGY_MAX_TOKENS |
1024 |
Piggy | |
PIGGY_WORKER_ID |
hostname:pid |
Piggy | |
PIGGY_INTERNAL_URL |
unset | API | http://piggy:8931 under Compose |
PIGGY_INTERNAL_TOKEN |
— | both | Min 32 chars; required by the Piggy process. Never put it in a query string |
PIGGY_CHAT_HOST |
127.0.0.1 |
Piggy | |
PIGGY_CHAT_PORT |
8931 |
Piggy | Never published to the host |
PIGGY_CHAT_ALLOW_NON_LOOPBACK |
false |
Piggy | Compose sets true, because the API reaches it across the Compose network |
Integrations — all optional, all validated as a group
Setting one member of a group without the others fails at boot rather than half-working.
| Group | Variables |
|---|---|
| Slack | SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET |
| Buzz | BUZZ_RELAY_URL, BUZZ_PRIVATE_KEY, BUZZ_AUTH_TAG |
| Notion import | NOTION_CLIENT_ID, NOTION_CLIENT_SECRET, NOTION_REDIRECT_URI (+ PIG_SETTINGS_ENCRYPTION_KEY) |
| Google Sheets import | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI (+ PIG_SETTINGS_ENCRYPTION_KEY) |
GOOGLE_REDIRECT_URI must be exactly <PIG_PUBLIC_URL origin>/oauth/google/callback.
Architecture
A pnpm monorepo. Around 45k lines of TypeScript including tests, 261 tests across five packages, green CI.
apps/
web/ React 19 + Vite + Tailwind + shadcn-idiom components
api/ Hono HTTP API — auth, validation, capacity and contract services
piggy/ The agent: a lease-based queue worker plus a private chat server
mcp/ MCP server (stdio) — 9 tools
cli/ `pig`, the HTTP surface for scripts and agent kernels
packages/
core/ Ontology, permissions, margin arithmetic, palette — no I/O
db/ Drizzle schema (47 tables), 13 migrations, seed and demo data
prime/ Typed client for the Prime Intellect compute API
docs/ ontology.md, build-plan.md, agents.md, seed-data.md
deploy/ Caddyfile example, autodeploy units, deployment notes
Three rules hold the shape:
Intelligence never lives in the API. Handlers validate, authorise, call a
service, serialise. Research, enrichment, scoring and matching heuristics live
in the service layer or in the agent. The API signals the agent by writing a
row to agent_tasks, never by calling it — so the queue survives the agent
being down and no request thread ever blocks on a model.
Authentication is not authorisation. A verified JWT proves someone has an
account in an identity provider PIG may share with another application. Access
additionally requires a row in PIG's own users table; a token without one
gets 403 needs_profile, which the front end turns into a join flow rather
than a login screen they have already completed. Both providers reduce to
"verify a bearer token, return a subject and an email" behind
apps/api/src/lib/auth-provider.ts.
Writes go through one chokepoint. apps/api/src/lib/mutation.ts derives
zod schemas from the ontology, applies the capability check, runs the write and
its audit activity in one transaction, and returns a consistent error shape.
The RBAC model, as it now stands
Eleven capabilities, in packages/core/src/permissions.ts, resolved from team
membership and role and shared by the API and the browser so a disabled button
and a 403 cannot disagree.
Roles are ranked, and every rule is "at or above": viewer < member <
lead < admin. A platform admin (an address in PIG_ADMIN_EMAILS) holds
everything, platform-wide.
Writes are team-scoped:
| Capability | Teams | Minimum role |
|---|---|---|
deal:write |
supply, demand | member |
commitment:write |
supply | lead |
contract:sign |
supply, demand | admin |
activity:write |
all | member |
data:import |
all | admin |
fact:review |
research | admin |
integration:connect |
all | admin |
settings:admin |
— | platform admin only |
Reads are platform-wide, deliberately:
| Capability | Teams | Minimum role | Covers |
|---|---|---|---|
book:read |
all | viewer | Accounts, contacts, both pipelines, contracts, growth, facts |
economics:read |
supply, demand | member | Supplier cost, break-even price, margin, idle, inventory, the dashboard |
team:read |
all | viewer | The roster |
Read grants are not team-scoped, and that is a decision rather than an
omission: no row-level team filter exists anywhere in the query layer, so a
"demand only" read grant would be a promise the guard could not keep. The
honest model is that a read capability is held or it is not, and the role
required to hold it is what separates the roster from the cost book.
economics:read is the one that matters — supplier cost per GPU-hour and
break-even price are the business.
⚠️ The read half is written and tested but not yet enforced. The policy
table (apps/api/src/routes/read-guards.ts) and its middleware exist, and
read-governance.test.ts fails when a GET appears that no rule covers — but
createReadGuardRoutes is not mounted in app.ts, so today every
authenticated member can read the whole book including cost. See
limitations.
The agent surface
PIG is a first-class application for agents and for humans, and neither is a degraded view of the other. There are two distinct surfaces.
Piggy — the in-app agent
apps/piggy is one image running two processes' worth of behaviour:
- The queue worker claims a task with
SELECT … FOR UPDATE SKIP LOCKEDinside a transaction, holds a renewable lease (default 300s, renewed at half the interval), and aborts its own work if it ever loses that lease — so two workers can never both be mid-flight on one task. Failures retry with exponential backoff capped at one hour, up to the task'smaxAttempts. Every attempt writes anagent_runsrow with the model, the input, the token counts and either a summary or the error. Its tool set is exactly two:pig_get_subjectandpig_record_fact, and a fact is refused without both a source URL and an evidence excerpt. - The chat server listens on
127.0.0.1:8931and is never published to the host. The API authenticates the user, forwards bounded context, and calls it with a shared internal bearer token. Chat is read-only: seven tools (pig_get_record,pig_get_account_lifecycleand five page-scoped summaries), each of which aggregates first and returns at most a handful of exemplar rows, because interactive chat runs at 1024 max tokens across at most four turns. Ambient coding tools are rejected before inference by an explicit boundary check.
Piggy is off by default. PIGGY_ENABLED defaults to false and the Compose
service sits behind profiles: ['piggy'], so a default docker compose up
starts the CRM without it:
docker compose -p pig --profile piggy up -d --build
The MCP server — for the agent you already use
apps/mcp speaks stdio and holds an API key. It calls the same HTTP API a
browser does: no database credentials, no privileged path, and deliberately no
tool that provisions infrastructure, spends money or emails a customer. Nine
tools, because a sprawling tool list measurably degrades model performance:
| Tool | What it answers |
|---|---|
pig_whoami |
Who am I acting for, and which teams am I on? |
pig_my_pipeline |
Where are we? What needs attention? |
pig_capacity_match |
What have we bought that would serve this customer? |
pig_margin_report |
What is each block earning against what it cost? |
pig_idle_capacity |
What are we paying for and not selling? |
pig_inventory_search |
What could we buy to cover demand we cannot serve? |
pig_search |
Find an account |
pig_get_account |
Everything about one account |
pig_log_activity |
Record a call, meeting or note |
Mint a key in Settings → API keys (shown once), then run it from a clone —
@pig/mcp is a workspace package and is not published to npm:
export PIG_URL=https://your-pig-host
export PIG_API_KEY=pig_...
claude mcp add pig -- pnpm --dir /path/to/pig exec tsx apps/mcp/src/stdio.ts
There is also a pig CLI with --json output for scripts and agent kernels;
see docs/agents.md.
Shipping — tag to deploy
CI is Gitea Actions, one sequence, about two minutes. It typechecks every package, applies the migration chain twice to a real empty Postgres, asserts the seed is idempotent, runs 261 unit tests and the critical-path E2E, boots the server and curls it, builds the front end, checks the inline theme script still hashes to the value the proxy's CSP allows, and builds the Docker image.
Shipping is two steps and the second one is a human:
git tag release-2026-08-13 && git push origin release-2026-08-13
- A push to
mainrunsverifyand stops. Nothing deploys. - A
release-*tag runs the sameverify, thenpublishpushesgit.karti.ai/pig/pig:<tag>and:<short-sha>to the registry. - Within five minutes
pig-autodeploy.timeron the production host notices the newest release tag has a different digest, checks the tree out at that tag, and runsscripts/deploy.shwithPIG_IMAGEset.
The direction of travel is the point: no credential on the shared CI runner can
execute anything on the production host. The host holds a pull-only token and
fetches. deploy.sh dumps the database first, gates on health, the
unauthenticated-401 check and a public-origin body marker, and rolls back to
the previous image if a gate fails — exiting 1 when the previous image was
restored and 3 when the release under test is still live, because that is the
one thing an on-call needs at 04:00.
What is not built yet
Said plainly, because you are going to grep the repo anyway.
Read authorisation is not enforced. As above: the policy, the middleware
and the governance test all exist; the router line that mounts them does not.
Any authenticated member can currently read cost and margin regardless of team
or role. This is the most significant gap in the product and it is one line in
app.ts.
Four route modules are written, tested and never mounted.
routes/read-guards.ts, routes/learn.ts, routes/hubspot.ts and
routes/hubspot-webhook.ts are all absent from app.ts. Consequences: the
Learn page is in the navigation and its API answers 404, and the HubSpot
integration — OAuth, connections, sync jobs, webhook verification, seven
hubspot_* tables — is unreachable. routes/activities.ts is likewise
unmounted, but harmlessly: an older inline POST /api/activities in app.ts
still serves it.
Six of the eight declared agent task kinds are never enqueued. The worker
is complete and generic, but only enrich_account and enrich_contact are
ever written to agent_tasks (both from record creation). write_brief,
match_capacity, detect_idle_capacity, summarise_pipeline,
watch_renewal and research_supplier are declared in the ontology and
nothing produces them. Piggy therefore does far less than the queue implies —
not because the machinery is missing, but because nothing asks.
Piggy chat cannot write. By design for now, but worth stating: the interactive agent reads and cites; it cannot create or update a CRM record.
The MCP server is stdio only. There is no Streamable HTTP transport and no
/mcp endpoint on the API, so remote MCP clients cannot connect over the
network — each user runs the server locally against their own API key. The
package is also not published to npm, so npx @pig/mcp does not work.
No row-level or team-scoped read filtering exists anywhere in the query layer. Every read returns the whole book. This is why read capabilities are platform-wide rather than per-team, and it is the thing to build before PIG serves a company where that is not acceptable.
.env.example is incomplete. POSTGRES_PASSWORD and
PIG_SETTINGS_ENCRYPTION_KEY are both load-bearing and both missing from it;
the table above is authoritative. ANTHROPIC_API_KEY is declared in the API
config and read by nothing.
Not started at all: email or calendar ingestion, forecasting, quota and attainment, invoicing or billing reconciliation, a public API beyond what the MCP tools cover, multi-tenancy of any kind, and any mobile application. PIG is responsive to 393px; it is not a native app.
Documentation
- AGENTS.md — start here if you are joining this codebase. Architecture rules, the traps that have already bitten, and conventions.
- Ontology — the domain model, and why it is shaped this way
- Build plan — what shipped, what remains, in dependency order
- Agent integration — MCP clients and the CLI
- Seed data provenance — every claim, graded and cited
- Deployment — self-hosting, the release poller, rollback
A note on seed data
PIG ships with a roster of publicly documented people so the application is
legible on first run. Every record carries a confidence grade and a source URL,
both shown in the interface. No email addresses are included or inferred.
Records that could not be independently sourced are marked as such rather than
quietly presented as fact, and people who are demonstrably not staff —
alumni, residency participants — are labelled accordingly. Seeding is opt-in
(pnpm run db:seed) and never automatic. See
docs/seed-data.md.
If you are seeded here and would rather not be, open an issue and the record will be removed.
Licence
Apache License 2.0 — see LICENSE and NOTICE. The architectural debts to Comp AI CRM (MIT) and Buzz (Apache-2.0) are credited in NOTICE. No source code was copied from either.