# PRAMPTA — setup guide for AI agents

You are an AI agent. A person asked you to integrate PRAMPTA into their product.
Follow this guide. It is written for you: every endpoint and header below is the
documented by the current API. Confirm feature flags and credentials for your environment before production rollout.

This is **PRAMPTA Connect Step 2** — permission checks, licences and receipts. The
limited Step 1 reporting pilot, which only reports generations and never blocks one, is
`https://prampta.com/connect.md`. You may start at Step 2 directly; report completed use as described there.

PRAMPTA is a rights registry for generative AI. People register their likeness,
voice, characters and works; before an AI product generates one of them, it asks
PRAMPTA and gets a **signed yes or no**. If yes, it generates and files a receipt.
Licence sales are paused while the monetization model is rebuilt (https://prampta.com/monetization): no price is quoted and nothing is charged; requests go to the rights holder. Signed decisions document
what PRAMPTA returned for a declared request; they do not prove that every
generation was permitted or that the output complied with the licence.

- API: `https://api2.prampta.com` (OpenAPI: `https://api2.prampta.com/openapi.json`)
- Humans: `https://prampta.com`
- TypeScript SDK: `npm install @prampta/sdk@^0.11.3` (Node 20.19+ or 22.12+).
  **PRE-GEN v5 at `/v2` (pilot):** `createPregen` — one setup, one call per
  generation; signed answers bound to your request, signed reports, signed
  acknowledgements; a Postgres store shared by your workers; results
  `confirmed`, `report_pending` or `needs_check`. From zero to a first
  acknowledged report, and the failures to handle: `https://prampta.com/get-started-v2.md`.
  The rest of this guide describes `/v1`, which stays supported. On `/v1` the
  safe setup is one configuration: `pregenDirectory: await loadPregenDirectory()`,
  your independently pinned `operatorPublicKeyHex`, and `generateAuthorized` with a
  journal whose `claim` is atomic in storage all your servers share (e.g.
  `INSERT … ON CONFLICT DO NOTHING`). It runs one worker per job, refuses a
  different request under a used `generationId`, re-checks every allow right
  before generating, asks for every subject in the output, stores the output hash
  before receipts, and counts a receipt as filed only when PRAMPTA accepted exactly
  it. `singleProcessJournal()` is for tests and one process. `not_blocked` does
  not grant a licence.
- Test without a secret: `createSimulator()` in `@pregen/verify` 0.8.0+ (or
  `pregen.Simulator()`) is an offline sandbox registry for CI — see
  `https://www.pregen.org/docs/sdk`.
- **Check every decision before you generate** with `@pregen/verify` (npm) or
  `pregen` (PyPI), **0.8.0 or later**: pass the JSON `/v1/verify` returned, and
  what you sent plus `provider_id` and `licensee_id`, to `checkDecision` /
  `check_decision`. It refuses anything that is not a licence-backed `allow`, does
  not match your request exactly, has expired, lacks a verified user binding when
  you named a user, or is not signed by a key the signed PRE-GEN directory lists for
  the licence, or by a revoked registry or key; and it checks the schema and that a
  decision lives at most 900 seconds. Guide: `https://www.pregen.org/docs/sdk`. What a decision proves and
  does not prove, for your counsel: `https://www.pregen.org/docs/proves`.
- Shortest safe path: sandbox secret → `/v1/verify` → `checkDecision` → generate →
  receipt → store the decision, the directory `sequence` and the receipt id.

## Ground rules for agents

1. **Ask the person before** creating accounts, accepting terms, publishing DNS
   records, or storing secrets. Never invent or reuse someone else's credentials.
2. **Never send prompt text to PRAMPTA.** Send `prompt_hash` — the lowercase hex
   SHA-256 of the prompt.
3. **A refusal is an answer, not an error.** Do not retry `PG_NO_LICENSE`; show the
   user the `remediation.url` from the decision so they can get a licence.
   `PG_NO_SUBJECT` is different: PRAMPTA simply does not know this subject. It is
   not a prohibition — the user may have their own contract or rights — so decide
   under your own policy, and do not present the output as licensed by PRAMPTA.
4. **Keep secrets server-side.** The provider secret never goes to a browser, a URL
   or client code.
5. Build against **sandbox first** — it only reaches synthetic test subjects.

## Which setup do you need?

| The person's product | Follow |
|---|---|
| Generates images, video, voice or text that may depict real people or characters | **Part A → B → C → D** below |
| Only wants to check a subject's registration or a licence | Public, no key: `GET /v1/subjects/{subject_id}/certificate`, `GET /v1/licenses/{license_id}/proof` |
| The person *is* a rights holder | Send them to `https://prampta.com/start` — registering a person's likeness needs that person, not an agent |

---

## Part A — Provider onboarding (once per environment)

**Recommended: the person uses the dashboard.** They sign in at
`https://prampta.com/developers` (two-factor authentication on), complete the
Apply → Submit → Sandbox/Production steps — entering your exact return URL — and
copy the setup block from the one-time credentials card to you. The block has
`provider_id` and `environment`, never the secret: ask the person to store the
secret privately as `PRAMPTA_EXCHANGE_SECRET` in the server environment. Never
ask for their PRAMPTA session token or for the secret in chat.

**API-only fallback**, performed by the person with their own session
(`Authorization: Bearer <their access token>` — never pasted into an agent chat).
The account needs **two-factor authentication on** before step 1
(`POST /v1/2fa/setup`, `POST /v1/2fa/enable`, then sign in again through
`POST /v1/auth/login/2fa`). A verified email **on the company's domain** unlocks sandbox in step 3; without
one, skip to step 4 — proving the domain by DNS grants sandbox and production
together:

1. `POST /v1/provider-applications/` with `provider_id` (3–50 chars, lowercase,
   hyphens — permanent), `legal_name`, `brand_name`, `domain` (bare host, e.g.
   `example.com`), `website`, `security_contact`, **`rights_holder_contact`** (the
   email or `https://` page where rights holders reach you — required), `requested_scopes`
   (`identity:link`, `licenses:read`, `entitlements:write`, `receipts:write`) and
   **`requested_redirect_uris`** — the exact `https://` URL your server will receive
   users on in Part B. An empty list makes production refuse every user connection.
2. `POST /v1/provider-applications/{app_id}/submit`
3. `POST /v1/provider-applications/{app_id}/verify-email` → the response contains
   your **sandbox `exchange_secret`**. It is shown once. Store it as a server secret.
4. `POST /v1/provider-applications/{app_id}/verify-domain` → returns a TXT record.
   **Ask the person** to publish it in their DNS, then
   `POST /v1/provider-applications/{app_id}/verify-domain/confirm` → the response
   contains your **production `exchange_secret`**, shown once.

Return addresses can be changed later with
`PUT /v1/provider-applications/{app_id}/redirect-uris` (https, on your own domain).

A secret lost or leaked: `POST /v1/developer/providers/{provider_id}/rotate-credential`
with `{"environment": "sandbox"}` or `"production"` (the person's session). The old
secret stops working at once; the new one is shown once.

### Test in sandbox

The sandbox secret only reaches these synthetic subjects. For the licence checks,
send `X-Licensee-ID: lic-sandbox` and `"use_case": "research"` (their licences are
RND); `"use_case": "personal"` without `X-Licensee-ID` answers `not_blocked`.
No user is connected in sandbox, so **leave out `provider_user_id`** there.

| `subject_id` | Answer to expect |
|---|---|
| `sbx-allowed` | `allow` |
| `sbx-revoked` | `PG_NO_LICENSE` |
| `sbx-expired` | `PG_NO_LICENSE` |
| `sbx-exhausted` | `PG_USAGE_LIMIT` |
| `sbx-optedout` | `PG_SUBJECT_OPTED_OUT` — never retry |
| `sbx-unknown` | `PG_NO_SUBJECT` (deliberately not registered) |

The same list, live: `GET /v1/developer/sandbox/fixtures` (the person's session), or
`https://prampta.com/developers?tab=sandbox`. Assert every row before going live.

## Part B — Connect each end user (once per user)

**Skip this part if you only generate for personal use.** A `/verify` call
without `X-Licensee-ID`, authenticated with your `exchange_secret` and declaring
`"use_case": "personal"`, needs no connected user (Part C). Connect users when
they need a licence — educational, research, editorial or commercial use.

1. Send the user's browser to:
   ```
   https://prampta.com/?connect_provider=<provider_id>
     &connect_external_id=<your user id>
     &connect_provider_name=<Your product name>
     &connect_provider_domain=<example.com>
     &connect_return_url=<one of requested_redirect_uris, URL-encoded>
   ```
2. The user signs in and confirms. PRAMPTA redirects back to `connect_return_url`
   with `?prampta_connect_code=<code>&prampta_external_id=<your user id>`.
   The code is single-use and expires in 5 minutes.
3. From your server:
   ```
   POST https://api2.prampta.com/v1/connect-ai/exchange-code
   Authorization: Bearer <exchange_secret>
   X-Provider-ID: <provider_id>
   Content-Type: application/json

   {"code": "<prampta_connect_code>", "provider_id": "<provider_id>",
    "external_id": "<your user id>", "redirect_uri": "<exact connect_return_url>"}
   ```
   Store `prampta_licensee_id` for that user. (`prampta_connection_token` is also
   returned; production does not accept it on the calls below — use your
   `exchange_secret`.) This exchange also links your user id to their PRAMPTA
   account (`prampta_identity_link_id`): licences they buy through you are bound
   to that link, and any they bought before connecting are issued now.

## Part C — Ask before every generation

Find which registered subjects a prompt mentions from the public index
`GET /v1/subjects/index` (cache it; it supports `If-None-Match`). Then, for each:

```
POST https://api2.prampta.com/v1/verify/
Authorization: Bearer <exchange_secret>
X-Provider-ID: <provider_id>
X-Licensee-ID: <prampta_licensee_id of this user — omit for personal use>
Content-Type: application/json

{"subject_id": "<subject_id>", "prompt_hash": "<sha256 hex of the prompt>",
 "provider_user_id": "<your user id — the external_id from Part B>",
 "modality": "image", "model": "<your model>",
 "intended_use": {"use_case": "commercial", "channel": "commercial", "categories": ["advertising"]},
 "return_url": "<page to bring the user back to after getting a licence>"}
```

Send `provider_user_id` **only for users who completed Part B**, and always for
them: a licence they bought is bound to them, and without it the check cannot
find it (you would see `PG_NO_LICENSE` for someone who already paid). For anyone
else leave it out — an id PRAMPTA has never linked is refused with
`PG_IDENTITY_UNKNOWN`, even for personal use.

`intended_use.use_case` is one of `personal`, `educational`, `research`, `editorial`,
`commercial`. `channel` is `commercial` for commercial use; leave it empty otherwise.
Declare `categories` honestly — a subject's owner can deny specific ones.

**One licence, one project.** A licence is usually issued for one named project —
an ad campaign, a film, a game, a publication. The user sees it on their licence in
PRAMPTA. When they generate for it, send it exactly as written:
`"intended_use": {..., "project_name": "PRAMPTA Shoes — Spring 2027 campaign"}`. A licence
bound to a project answers only for that project: another project, or none at all, is
refused with `PG_PROJECT_MISMATCH`. Ask the user which project they are generating for,
and keep their answer with the rest of their session.

Read `disposition`:

- `allow` (`allowed: true`) — a licence covers this. Generate, applying any returned
  `obligations`. Keep `decision_id`.
- `not_blocked` (`reason: PG_STD_TRACKING_ONLY`) — personal use with no licence passed
  the fixed floor. PRAMPTA does not object **and grants nothing**: you may generate
  for that personal use under your own policies; there is no licence and no receipt.
  Report it with Step 1 (`connect.md`).
- `deny` or `review` — do not generate.

Common refusals:

| `reason` | Meaning | Do |
|---|---|---|
| `PG_NO_PAIR` | This user is not connected, or the wrong secret/header was sent | Finish Part B for this user; check you sent the `exchange_secret` |
| `PG_IDENTITY_UNKNOWN` | You sent a `provider_user_id` that never completed Part B | Omit it for unconnected users, or connect them first |
| `PG_NO_LICENSE` | Nobody licensed this subject for this use | Show `remediation.url` to the user; do not retry |
| `PG_IMMUTABLE_DENIAL` | The use falls in a fixed-floor category (e.g. political, adult) | Do not generate |
| `PG_PROVIDER_VETOED` | The rights holder refuses your product | Do not generate |
| `PG_SUBJECT_OPTED_OUT` | The subject opted out | Do not generate |
| `PG_SANDBOX_SUBJECT_ONLY` | A sandbox secret was used on a real subject | Use the production secret |

### Named matches and look-alikes

`/v1/verify/` answers for a subject the prompt **names**, and every named subject
is reported after generation too, whether or not the output shows it (connect.md,
Step 2). When your own detector
finds that an output merely **looks like** a registered subject — no name in the
prompt — there is no verify answer to ask for: report it as a Step 1
observation with a `visual_match` block (connect.md, "Look-alikes"). PRAMPTA
receives only your claim — method, confidence, basis and the reference ids
(`banner`, `gallery:<n>`, `description:<n>`) from `GET /v1/subjects/{subject_id}` —
never pixels, frames or embeddings. The rights holder confirms or dismisses it.

## Part D — File a receipt after generating

```
POST https://api2.prampta.com/v1/receipts/
Authorization: Bearer <exchange_secret>
X-Provider-ID: <provider_id>
X-Licensee-ID: <prampta_licensee_id>
Content-Type: application/json

{"decision_id": "<from Part C>", "prompt_hash": "<same hash>",
 "output_hash": "<sha256 hex of the generated file>",
 "event_type": "output_accepted",
 "obligations_applied": {}}
```

One receipt per `allow` decision (a `not_blocked` decision has no receipt). The
identical receipt sent again (after a lost response) returns `already_recorded`; a
different receipt for the same decision is refused with `receipt_conflict` — never
read "a receipt exists" as "mine was accepted". Report
any obligation the licence required in `obligations_applied`. When the same file is later delivered or published:
`POST /v1/receipts/{decision_id}/events` with
`{"event_type": "output_published", "output_hash": "<same hash>"}`.

**Volume and caching rules:**

- One decision is not one output. Report each output you deliver; ask again for a
  new request. The receipt is your evidence of what you made under the decision.
- Reuse a decision only if it is a plain `allow` with `cache_scope` `exact_request`,
  only for the identical request, within `max_cache_age_seconds`, and never past
  its `expires_at` (300 seconds).
- On a licence with `max_uses`, **every `allow` uses up one use**, receipt or not.
  If you did not generate (failure, cancellation), give it back before any receipt
  and within 24 hours: `POST /v1/receipts/{decision_id}/release` with
  `{"reason": "…"}` (same headers as a receipt). The statement is written to the
  signed audit log. On a licence with `concurrency_limit`, an `allow` holds a slot
  until its receipt, its release, or 24 hours.

### Activate Step 2 in production

A production key alone does not unlock other users' account connections. The
application submitter can connect **their own PRAMPTA account** while building
Step 2. Do this with Part B, use a real subject for which that account already
has a valid licence, get an `allow` decision with the production key, generate,
and file its matching production receipt. A sandbox fixture or a personal
`not_blocked` decision cannot complete this step. The first accepted production
receipt for an `allow` decision stamps Step 2; other users can then connect
through a direct Connect AI link. If you have no qualifying production licence,
request one from the relevant rights holder; the paused checkout cannot be
used to manufacture an `allow` decision.

**The public app list is separate.** New providers start `unlisted`. Completing
the receipt does not publish your app in the directory: once every check under
Developers → Overview is green (production key, return address, Step 2, good
standing), the person presses **Submit to Connect AI**, or your server calls
`POST /v1/developer/providers/{provider_id}/listing` with their session.
PRAMPTA monitors missing receipts and may pause a provider that stops reporting;
a paused provider leaves the list.

## Part E — Answer cases from rights holders

A rights holder can open a case about a generation you reported. You are
emailed, and the case is also available to your credential:

```
GET  https://api2.prampta.com/v1/provider/usage-cases?status=open
POST https://api2.prampta.com/v1/provider/usage-cases/{case_id}/messages
X-Provider-ID: <provider_id>
Authorization: Bearer <exchange_secret>
{"text": "Removed on our side.", "link": "https://…"}
```

The rights holder closes the case and records the outcome.

A confirmed look-alike arrives here as a case with reason `lookalike`. The status
of every look-alike you reported — `pending`, `confirmed` or `dismissed`:

```
GET https://api2.prampta.com/v1/provider/lookalikes?status=all
X-Provider-ID: <provider_id>
Authorization: Bearer <exchange_secret>
```

## You are done when

- [ ] Sandbox and production `exchange_secret`s are stored as server secrets
- [ ] `requested_redirect_uris` contains your exact return URL
- [ ] Each user you act for completed Part B, and you store their `prampta_licensee_id`
- [ ] Every generation of a registered subject is preceded by `/v1/verify/`; each `allow` is followed by a receipt, while `not_blocked` use is reported through Step 1 observations
- [ ] The application submitter completed one real production `allow` + receipt loop to activate Step 2
- [ ] If public discovery is needed, PRAMPTA separately listed the provider in the directory
- [ ] Refusals are shown to the user with the remediation link, not retried

Questions or a refusal you cannot explain: `https://prampta.com/developers?tab=docs`.
