feat(prometheus+spawn): Prometheus engine (stubbed) + manual Pump.fun creator

Built by 2 parallel agents (+ image-API research):
- @pyre/prometheus: generateSpawn() engine — deterministic §9 meta-mixer
  (40/25/20/15), prompt builder ("inspired mutation, not a clone" + no
  people/brands), name/ticker/lore/tagline gen, image-prompt, denylist + moderation
  safety. PROVIDER-ABSTRACTED (TextProvider/ImageProvider/ModerationProvider) with
  deterministic STUBS so it runs keyless today; real call shapes documented (Claude
  Haiku text · FLUX schnell image · OpenAI omni-moderation). 13 tests.
- @pyre/db: migration 002 (prometheus_generations, spawn_records) + record/list/get.
- @pyre/api: admin-gated POST /api/prometheus/generate + /api/spawn/launch
  (x-admin-token; CLOSED with 403 when ADMIN_API_TOKEN unset; timing-safe compare),
  public GET /api/spawns + /api/spawn/:id.
- @pyre/web: public /spawn record page; @pyre/core SpawnRecord type.

Verified: typecheck 8/8, 134 tests (core 91 + prometheus 13 + solana 30), web build
(+/spawn), migrate 002 live, /api/spawns OK, admin gate returns 403 (unconfigured).
Follow-ups: set ADMIN_API_TOKEN to use admin endpoints; wire real provider keys;
receiptId→DB-id wiring; admin generation UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 07:09:53 +00:00
parent 28064c5131
commit 8b58faf7c1
16 changed files with 1882 additions and 76 deletions

View File

@@ -0,0 +1,48 @@
-- 002_spawns.sql — Prometheus generations + Spawn records (idempotent).
--
-- Backs the manual / approval-gated Pump.fun creator workflow (§10, §15):
-- prometheus_generations — every Spawn package Prometheus generates, with the
-- mixer input, the raw output, its review status, and the risk flags raised.
-- spawn_records — the public, immutable record of a Spawn whose token
-- was MANUALLY created on Pump.fun by the operator (mint / metadata URI /
-- Pump.fun URL / launch tx). PYRE never signs the launch (§3) — it only
-- records it.
--
-- Safe to run repeatedly: every object uses IF NOT EXISTS. JSON columns are
-- JSONB; the TypeScript layer marshals them as parsed objects.
CREATE TABLE IF NOT EXISTS prometheus_generations (
id BIGSERIAL PRIMARY KEY,
-- The cleanup receipt this generation drew its burned-token context from.
-- Nullable: a generation can be seeded from a minimal operator-supplied input
-- with no backing receipt.
receipt_id BIGINT,
input_json JSONB NOT NULL,
output_json JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'generated'
CHECK (status IN ('generated', 'approved', 'rejected', 'launched')),
risk_flags_json JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
approved_at TIMESTAMPTZ,
rejected_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS spawn_records (
id BIGSERIAL PRIMARY KEY,
generation_id BIGINT NOT NULL REFERENCES prometheus_generations(id),
spawn_name TEXT NOT NULL,
ticker TEXT NOT NULL,
mint TEXT,
metadata_uri TEXT,
pumpfun_url TEXT,
launch_tx TEXT,
status TEXT NOT NULL DEFAULT 'launched'
CHECK (status IN ('launched', 'pending')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_prometheus_generations_receipt_id
ON prometheus_generations (receipt_id);
CREATE INDEX IF NOT EXISTS idx_spawn_records_generation_id
ON spawn_records (generation_id);

View File

@@ -341,6 +341,375 @@ export async function getEssenceSummary(): Promise<EssenceSummary> {
return { roundId, totalLamports, contributionCount, recent };
}
// ===========================================================================
// Prometheus generations + Spawn records (§10, §15) — manual Pump.fun workflow.
//
// The MVP Pump.fun launch is MANUAL / approval-gated: Prometheus generates a
// Spawn package (recorded in `prometheus_generations`), an operator reviews it
// and creates the token on Pump.fun by hand, then records the launch
// identifiers (`spawn_records`). PYRE never holds keys and never signs a launch
// (§3) — it only records what the operator did.
// ===========================================================================
/** Lifecycle status of a `prometheus_generations` row. */
export type GenerationStatus = "generated" | "approved" | "rejected" | "launched";
/** Input for {@link recordGeneration}. */
export interface RecordGenerationInput {
/**
* The `cleanup_receipts.id` this generation drew burned-token context from, or
* `null` when seeded from a minimal operator-supplied input.
*/
receiptId?: string | null;
/** The assembled Prometheus mixer input (persisted as JSONB). */
input: unknown;
/** The Prometheus generation output (persisted as JSONB). */
output: unknown;
/** Safety/compliance flags raised during generation. */
riskFlags: string[];
}
/** A persisted `prometheus_generations` row. */
export interface GenerationRecord {
id: string;
receiptId: string | null;
input: unknown;
output: unknown;
status: GenerationStatus;
riskFlags: string[];
createdAt: string;
approvedAt: string | null;
rejectedAt: string | null;
}
/** Input for {@link recordSpawnLaunch}. */
export interface RecordSpawnLaunchInput {
/** The `prometheus_generations.id` this Spawn was launched from. */
generationId: string;
spawnName: string;
ticker: string;
/** SPL mint address of the launched token (base58). */
mint?: string;
/** URI of the token metadata JSON. */
metadataUri?: string;
/** Public Pump.fun page for the token. */
pumpfunUrl?: string;
/** Confirmed launch (create) transaction signature. */
launchTx?: string;
}
/** A persisted `spawn_records` row. */
export interface SpawnRecordRow {
id: string;
generationId: string;
spawnName: string;
ticker: string;
mint: string | null;
metadataUri: string | null;
pumpfunUrl: string | null;
launchTx: string | null;
status: "launched" | "pending";
createdAt: string;
}
/**
* Burned-token context derived from a persisted cleanup receipt, used to seed a
* Prometheus generation when the operator supplies a `receiptId`.
*/
export interface ReceiptContext {
wallet: string;
kind: ReceiptKind;
/** Addresses of the accounts closed/burned by the receipt's transaction. */
closedAccounts: string[];
}
/** Postgres `to_char` format producing an ISO-8601 timestamp with offset. */
const ISO_TS = `'YYYY-MM-DD"T"HH24:MI:SS.MSOF'`;
/**
* Look up burned-token context from a persisted `cleanup_receipts` row by its id.
*
* Returns `null` when no such receipt exists. The `receiptId` is the receipt
* row's BIGSERIAL id (string form); non-numeric ids never match.
*/
export async function getReceiptContext(
receiptId: string,
): Promise<ReceiptContext | null> {
if (!/^\d+$/.test(receiptId)) return null;
const db = getPool();
const res = await db.query<{
wallet: string;
kind: string;
closed_accounts: unknown;
}>(
`SELECT wallet, kind, closed_accounts
FROM cleanup_receipts
WHERE id = $1::bigint`,
[receiptId],
);
const row = res.rows[0];
if (row === undefined) return null;
const closedAccounts = Array.isArray(row.closed_accounts)
? (row.closed_accounts as unknown[]).filter(
(v): v is string => typeof v === "string",
)
: [];
return {
wallet: row.wallet,
kind: row.kind as ReceiptKind,
closedAccounts,
};
}
/**
* Persist a Prometheus generation (the mixer input, the raw output, and the
* risk flags) and return the new row's id.
*
* Status starts at `'generated'`. JSON columns are stored as JSONB via
* parameterized `::jsonb` casts — never string-interpolated.
*/
export async function recordGeneration(
g: RecordGenerationInput,
): Promise<{ id: string }> {
const db = getPool();
const res = await db.query<{ id: string }>(
`INSERT INTO prometheus_generations
(receipt_id, input_json, output_json, risk_flags_json)
VALUES ($1::bigint, $2::jsonb, $3::jsonb, $4::jsonb)
RETURNING id::text AS id`,
[
g.receiptId != null && /^\d+$/.test(g.receiptId) ? g.receiptId : null,
JSON.stringify(g.input),
JSON.stringify(g.output),
JSON.stringify(g.riskFlags),
],
);
const row = res.rows[0];
if (row === undefined) throw new Error("failed to record generation");
return { id: row.id };
}
/** Fetch a single Prometheus generation by id, or `null` if it does not exist. */
export async function getGeneration(
id: string,
): Promise<GenerationRecord | null> {
if (!/^\d+$/.test(id)) return null;
const db = getPool();
const res = await db.query<{
id: string;
receipt_id: string | null;
input_json: unknown;
output_json: unknown;
status: string;
risk_flags_json: unknown;
created_at: string;
approved_at: string | null;
rejected_at: string | null;
}>(
`SELECT
id::text AS id,
receipt_id::text AS receipt_id,
input_json,
output_json,
status,
risk_flags_json,
to_char(created_at, ${ISO_TS}) AS created_at,
to_char(approved_at, ${ISO_TS}) AS approved_at,
to_char(rejected_at, ${ISO_TS}) AS rejected_at
FROM prometheus_generations
WHERE id = $1::bigint`,
[id],
);
const row = res.rows[0];
if (row === undefined) return null;
return {
id: row.id,
receiptId: row.receipt_id,
input: row.input_json,
output: row.output_json,
status: row.status as GenerationStatus,
riskFlags: Array.isArray(row.risk_flags_json)
? (row.risk_flags_json as unknown[]).filter(
(v): v is string => typeof v === "string",
)
: [],
createdAt: row.created_at,
approvedAt: row.approved_at,
rejectedAt: row.rejected_at,
};
}
/**
* Record a MANUAL Pump.fun launch: the operator created the token by hand and is
* now filing the immutable public record (mint / metadata URI / Pump.fun URL /
* launch tx). Marks the originating generation `'launched'` in the same
* transaction. Returns the new Spawn record.
*
* PYRE never signs the launch (§3) — these are recorded facts, not actions.
*/
export async function recordSpawnLaunch(
s: RecordSpawnLaunchInput,
): Promise<SpawnRecordRow> {
const db = getPool();
const client = await db.connect();
try {
await client.query("BEGIN");
const inserted = await client.query<{
id: string;
generation_id: string;
spawn_name: string;
ticker: string;
mint: string | null;
metadata_uri: string | null;
pumpfun_url: string | null;
launch_tx: string | null;
status: string;
created_at: string;
}>(
`INSERT INTO spawn_records
(generation_id, spawn_name, ticker, mint, metadata_uri, pumpfun_url, launch_tx, status)
VALUES ($1::bigint, $2, $3, $4, $5, $6, $7, 'launched')
RETURNING
id::text AS id,
generation_id::text AS generation_id,
spawn_name,
ticker,
mint,
metadata_uri,
pumpfun_url,
launch_tx,
status,
to_char(created_at, ${ISO_TS}) AS created_at`,
[
s.generationId,
s.spawnName,
s.ticker,
s.mint ?? null,
s.metadataUri ?? null,
s.pumpfunUrl ?? null,
s.launchTx ?? null,
],
);
const row = inserted.rows[0];
if (row === undefined) throw new Error("failed to record spawn launch");
// Mark the originating generation as launched (best-effort within the tx).
await client.query(
`UPDATE prometheus_generations SET status = 'launched' WHERE id = $1::bigint`,
[s.generationId],
);
await client.query("COMMIT");
return mapSpawnRow(row);
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
}
/** Map a raw `spawn_records` row to a {@link SpawnRecordRow}. */
function mapSpawnRow(row: {
id: string;
generation_id: string;
spawn_name: string;
ticker: string;
mint: string | null;
metadata_uri: string | null;
pumpfun_url: string | null;
launch_tx: string | null;
status: string;
created_at: string;
}): SpawnRecordRow {
return {
id: row.id,
generationId: row.generation_id,
spawnName: row.spawn_name,
ticker: row.ticker,
mint: row.mint,
metadataUri: row.metadata_uri,
pumpfunUrl: row.pumpfun_url,
launchTx: row.launch_tx,
status: row.status as "launched" | "pending",
createdAt: row.created_at,
};
}
/**
* List Spawn records, newest first. `limit` is clamped to 1..100 (default 50).
* Public / read-only.
*/
export async function listSpawns(limit = 50): Promise<SpawnRecordRow[]> {
const safeLimit = Math.min(100, Math.max(1, Math.trunc(limit) || 50));
const db = getPool();
const res = await db.query<{
id: string;
generation_id: string;
spawn_name: string;
ticker: string;
mint: string | null;
metadata_uri: string | null;
pumpfun_url: string | null;
launch_tx: string | null;
status: string;
created_at: string;
}>(
`SELECT
id::text AS id,
generation_id::text AS generation_id,
spawn_name,
ticker,
mint,
metadata_uri,
pumpfun_url,
launch_tx,
status,
to_char(created_at, ${ISO_TS}) AS created_at
FROM spawn_records
ORDER BY id DESC
LIMIT $1`,
[safeLimit],
);
return res.rows.map(mapSpawnRow);
}
/** Fetch a single Spawn record by id, or `null` if it does not exist. */
export async function getSpawnRecord(id: string): Promise<SpawnRecordRow | null> {
if (!/^\d+$/.test(id)) return null;
const db = getPool();
const res = await db.query<{
id: string;
generation_id: string;
spawn_name: string;
ticker: string;
mint: string | null;
metadata_uri: string | null;
pumpfun_url: string | null;
launch_tx: string | null;
status: string;
created_at: string;
}>(
`SELECT
id::text AS id,
generation_id::text AS generation_id,
spawn_name,
ticker,
mint,
metadata_uri,
pumpfun_url,
launch_tx,
status,
to_char(created_at, ${ISO_TS}) AS created_at
FROM spawn_records
WHERE id = $1::bigint`,
[id],
);
const row = res.rows[0];
if (row === undefined) return null;
return mapSpawnRow(row);
}
/**
* Close and clear the singleton pool. Intended for graceful shutdown / test
* teardown; a subsequent {@link getPool} call lazily creates a fresh pool.