PRE-GEN

Integrate

HTTP · verification: @pregen/verify (npm) and pregen (PyPI) 0.8.0 or later · reference TypeScript SDK @prampta/sdk 0.11.3 (Apache-2.0)

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

PackageUse at leastWhy
@pregen/verify0.8.0checkDecision: 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).
pregen0.8.0the same, for Python
@prampta/sdk0.11.3generateAuthorized 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 with receipt_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_scope exact_request may be reused, only for the identical request, only within max_cache_age_seconds, and never past its expires_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 with POST /v1/receipts/{decision_id}/release; the statement is kept in the signed audit log. An allow on a licence with concurrency_limit holds 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_idExpected answer (research use, lic-sandbox)
sbx-allowedallow
sbx-revokedPG_NO_LICENSE
sbx-expiredPG_NO_LICENSE
sbx-exhaustedPG_USAGE_LIMIT
sbx-optedoutPG_SUBJECT_OPTED_OUT — hard, never retry
sbx-unknownPG_NO_SUBJECT

Full step-by-step guide for providers: prampta.com/get-started.md. Wire contract: OpenAPI.