Integrate
A provider needs credentials from a registry. The first registry, PRAMPTA, issues a sandbox key after a verified company email and a production key after a DNS record on the company domain. The sandbox reaches synthetic test subjects only.
Minimum safe versions
| Package | Use at least | Why |
|---|---|---|
| @pregen/verify | 0.8.0 | checkDecision: signature by a key the directory lists for the namespace, not revoked (V-14); exact request match; supported schema_version; issued_at and expires_at at most 900 seconds apart; no unknown critical member (E-3); optional requireVerifiedAuthority. Directory: continuity, revocations kept across a steward handover (0.7.0: a thief's later revocations can be annulled), pinned PRAMPTA keys, successor steward key. Offline simulator with PRAMPTA's receipt rules (0.8.0). |
| pregen | 0.8.0 | the same, for Python |
| @prampta/sdk | 0.11.3 | generateAuthorized with a required journal: one worker per job through an atomic claim, one request per generationId, a re-check right before generating, the output hash stored before receipts, a receipt counted as filed only when the registry accepted exactly it. Strict decision checks with or without pregenDirectory. Also createPregen for PRAMPTA's /v2 pilot of the simplified formats, which are a draft and not yet part of this specification. Needs Node 20.19+ or 22.12+. |
The shortest safe path: install the SDK → sandbox secret → the setup below, which turns on every check → generateAuthorized. Over raw HTTP: /v1/verify → checkDecision → generate → receipt, and you build the job rules yourself.
Two libraries, two jobs: @prampta/sdk asks the PRAMPTA registry for a decision; @pregen/verify (on PyPI: pregen) checks that any registry's answer is genuine.
Keep the provider secret on your server. It never goes to a browser, a URL or client code. Send a hash of the prompt, never the prompt.
Ask before generating
POST https://api2.prampta.com/v1/verify/
Authorization: Bearer $PROVIDER_SECRET
X-Provider-ID: your-provider-id
X-Licensee-ID: lic-… # the connected user; omit for personal use
Content-Type: application/json
{"subject_id": "sbx-allowed",
"prompt_hash": "<sha256 hex of the prompt>",
"modality": "image",
"intended_use": {"use_case": "research"}}
Read disposition. Generate only on allow (or, for personal use, not_blocked under your own policy). On deny show remediation.url when present: it is where the user can request a licence.
Report after generating
POST https://api2.prampta.com/v1/receipts/
Authorization: Bearer $PROVIDER_SECRET
X-Provider-ID: your-provider-id
X-Licensee-ID: lic-…
{"decision_id": "dec_…", "prompt_hash": "…", "output_hash": "<sha256 of the file>",
"event_type": "output_accepted", "obligations_applied": {}}
One receipt per allow decision. The identical receipt sent again returns already_recorded; a different one is refused (receipt_conflict).
Check the signature against PRE-GEN
A signed allow is worth something only if the key that signed it belongs to the registry that owns the code. The verification library checks that chain with the PRE-GEN steward key built in: the registry directory is signed by the steward, the code's namespace belongs to one listed registry, the decision's key is one of that registry's keys, and the signature verifies.
npm install @pregen/verify
import { loadDirectory, checkDecision } from "@pregen/verify";
const directory = await loadDirectory({ previous: saved }); // keep the last one you accepted
saved = directory.toJSON();
// request: what you sent to /v1/verify, plus provider_id and licensee_id
await checkDecision(decision, request, { directory }); // throws with the reason
pip install pregen
import pregen
directory = pregen.load_directory(previous=saved)
saved = directory.to_json()
pregen.check_decision(decision, request, directory) # raises with the reason
checkDecision runs every check of PRE-GEN-SPEC.md §5.4 before you generate: a license-backed allow, every echoed field equal to what you sent, a valid lifetime, a verified user binding when you named a user, and the signature chain. It refuses an older or shrunk directory, and the origin registry's keys are pinned in the library. Source and tests: packages/, Apache-2.0.
Test without a secret
The verification libraries include an offline simulator: a stand-in registry with throwaway keys that answers the sandbox subjects below and handles receipts by PRAMPTA's rules (0.8.0): a receipt needs an allow issued to you for the same prompt, the identical receipt again returns the first answer, a different one is a conflict, and receipt_hash binds each answer to the bytes you sent. Your CI and coding agents can run the whole pipeline — ask, checkDecision, generate, receipt — without a provider secret. Nothing the simulator signs is ever accepted by the real directory.
import { createSimulator, checkDecision } from "@pregen/verify"; // 0.8.0 or later
const sim = await createSimulator();
globalThis.fetch = sim.fetch; // your code calls sim.baseUrl + "/v1/verify/"
await checkDecision(decision, request, { directory: sim.directory, fetch: sim.fetch });
sim = pregen.Simulator() # pip install "pregen>=0.8.0"
decision = sim.verify(request, provider_id="acme", licensee_id="lic-1")
pregen.check_decision(decision, {**request, "provider_id": "acme", "licensee_id": "lic-1"}, sim.directory, sim.get_json)
status, ack = sim.receipt(receipt, "acme", "lic-1") # (200, {"status": "recorded", "receipt_hash": ...})
Then switch to the real sandbox with a secret, and to production. The simulator proves your client handles every answer; only the real registry proves your credentials and connections.
Receipts, volume and caching
- One receipt per allow. Sending the identical receipt again (after a lost response) returns
already_recorded; a different receipt for the same decision is refused withreceipt_conflict. Never read "a receipt exists" as "mine was accepted". - One decision is not one output. The protocol cannot stop several generations under one allow; report each output you deliver and ask again for a new request. Lifecycle events after the receipt follow one output.
- Caching. Only a plain allow with
cache_scopeexact_requestmay be reused, only for the identical request, only withinmax_cache_age_seconds, and never past itsexpires_at(300 seconds at PRAMPTA). - Usage limits at PRAMPTA. On a licence with
max_uses, every allow uses up one use, receipt or not. If you did not generate, give it back before any receipt and within 24 hours withPOST /v1/receipts/{decision_id}/release; the statement is kept in the signed audit log. An allow on a licence withconcurrency_limitholds a slot until its receipt, its release, or 24 hours. - Without receipts you keep only the decisions: proof of what you asked, not of what you made. A provider becomes available to users outside its own team only after one production allow followed by its receipt.
TypeScript SDK
npm install @prampta/sdk@^0.11.3
The main setup: the signed PRE-GEN directory, your independently pinned operator key, and a journal in your own database whose claim is atomic across all your servers.
import { Prampta, loadPregenDirectory } from "@prampta/sdk";
const pregenDirectory = await loadPregenDirectory({ previous: saved }); // steward key built in
const pg = new Prampta({
baseUrl: "https://api2.prampta.com", providerId, licenseeId,
token: process.env.PRAMPTA_TOKEN, // server-side only
operatorPublicKeyHex: process.env.PRAMPTA_OPERATOR_PUBLIC_KEY, // obtained independently
pregenDirectory,
});
// claim: INSERT ... ON CONFLICT DO NOTHING; if no row was inserted, return the stored entry
const journal = {
claim: async (e) => (await db.insertIfAbsent(e.generationId, e)) ? null : db.get(e.generationId),
put: async (e) => db.update(e.generationId, e),
};
const result = await pg.generateAuthorized(
{ subjectIds: [subjectCode], generationId: jobId, promptHash, model, modality: "image",
intendedUse: { useCase: "commercial" } },
async (decisions) => { const image = await render(prompt); return { output: image, outputBytes: image.bytes }; },
{ journal },
);
// result.unreported: call again with the same jobId and request. result.conflicts: investigate.
A journal is not a lock unless its claim is atomic in storage every worker shares. singleProcessJournal() is for tests and one process only.
verify(), assertAllowed() and assertLicensed() are lower-level: they check one decision and leave claiming, retries and receipts to you. Without pregenDirectory they check the signature against your pinned key but not the directory (namespace, revocation).
Sandbox test subjects
| subject_id | Expected answer (research use, lic-sandbox) |
|---|---|
| sbx-allowed | allow |
| sbx-revoked | PG_NO_LICENSE |
| sbx-expired | PG_NO_LICENSE |
| sbx-exhausted | PG_USAGE_LIMIT |
| sbx-optedout | PG_SUBJECT_OPTED_OUT — hard, never retry |
| sbx-unknown | PG_NO_SUBJECT |
Full step-by-step guide for providers: prampta.com/get-started.md. Wire contract: OpenAPI.