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

@@ -942,3 +942,118 @@ body {
background: var(--color-coal) !important;
border: 1px solid var(--color-ember) !important;
}
/* ---- Public Spawn record page (/spawn) ---- */
.spawn-page {
position: relative;
text-align: center;
}
.spawn-page__glow {
position: absolute;
inset: -2rem -2rem auto;
height: 8rem;
background: radial-gradient(60% 100% at 50% 0, rgba(255, 87, 34, 0.18), transparent 70%);
pointer-events: none;
}
.spawn-page__intro {
max-width: 40rem;
margin: 0 auto 2.5rem;
color: var(--color-smoke);
font-size: 0.95rem;
line-height: 1.7;
}
.spawn-page__note,
.spawn-page__empty {
color: var(--color-smoke);
font-size: 0.95rem;
margin: 2rem 0;
}
.spawn-page__empty {
color: var(--color-ember-bright);
font-weight: 600;
}
.spawn-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1.25rem;
text-align: left;
}
.spawn-card {
background: var(--color-coal);
border: 1px solid rgba(255, 87, 34, 0.25);
border-radius: 0.9rem;
overflow: hidden;
display: flex;
flex-direction: column;
}
.spawn-card__img {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
display: block;
}
.spawn-card__img--placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
background: radial-gradient(80% 80% at 50% 30%, rgba(255, 87, 34, 0.2), var(--color-ash));
}
.spawn-card__body {
padding: 1rem 1.1rem 1.2rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.spawn-card__name {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
color: #f5ede6;
}
.spawn-card__ticker {
color: var(--color-ember-bright);
font-weight: 700;
font-size: 0.9rem;
}
.spawn-card__lore {
margin: 0;
color: var(--color-smoke);
font-size: 0.85rem;
line-height: 1.55;
}
.spawn-card__meta {
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.8rem;
}
.spawn-card__row {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.spawn-card__row dt {
color: var(--color-smoke);
}
.spawn-card__row dd {
margin: 0;
color: #f5ede6;
font-variant-numeric: tabular-nums;
}
.spawn-card__link {
margin-top: 0.3rem;
align-self: flex-start;
color: var(--color-ember-bright);
font-weight: 600;
font-size: 0.85rem;
text-decoration: none;
}
.spawn-card__link:hover {
text-decoration: underline;
}

View File

@@ -0,0 +1,144 @@
"use client";
import { useEffect, useState } from "react";
import { Footer } from "../../components/Footer";
// Same-origin by default so production hits "/api/spawns" behind the same host.
// Override with NEXT_PUBLIC_API_URL only when the API lives elsewhere (e.g. dev).
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
/** Public Spawn record shape, mirroring `@pyre/core`'s `SpawnRecord`. */
type SpawnRecord = {
id: string;
generationId: string;
spawnName: string;
ticker: string;
mint?: string;
metadataUri?: string;
pumpfunUrl?: string;
launchTx?: string;
status: "launched" | "pending";
createdAt: string;
imageUrl?: string;
lore?: string;
};
type SpawnsResponse = { spawns: SpawnRecord[] };
function truncate(addr: string): string {
if (addr.length <= 10) return addr;
return `${addr.slice(0, 4)}${addr.slice(-4)}`;
}
/**
* Public, read-only Spawn record page (§10, §18 Phase 5). Lists Spawns whose
* tokens were MANUALLY created on Pump.fun by the operator and recorded here.
* No investment, yield, or profit is implied — ritual/entertainment framing.
*/
export default function SpawnPage() {
const [spawns, setSpawns] = useState<SpawnRecord[] | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let active = true;
(async () => {
try {
const res = await fetch(`${API_BASE}/api/spawns`);
if (!res.ok) throw new Error(`spawns fetch failed (${res.status})`);
const data = (await res.json()) as SpawnsResponse;
if (active) {
setSpawns(data.spawns ?? []);
setFailed(false);
}
} catch {
if (active) {
setSpawns([]);
setFailed(true);
}
}
})();
return () => {
active = false;
};
}, []);
const loading = spawns === null && !failed;
const empty = !loading && (spawns === null || spawns.length === 0);
return (
<main className="page">
<section className="spawn-page" aria-labelledby="spawn-heading">
<div className="spawn-page__glow" aria-hidden="true" />
<h1 className="section-heading" id="spawn-heading">
The Spawn
</h1>
<p className="spawn-page__intro">
Tokens reborn from burned remnants generated by Prometheus, reviewed
by hand, and created on Pump.fun by the operator. This is a public
record, not an investment. Entertainment only.
</p>
{loading ? (
<p className="spawn-page__note">Reading the embers</p>
) : empty ? (
<p className="spawn-page__empty">no Spawns yet feed the PYRE 🔥</p>
) : (
<ul className="spawn-list">
{spawns!.map((s) => (
<li className="spawn-card" key={s.id}>
{s.imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
className="spawn-card__img"
src={s.imageUrl}
alt={`${s.spawnName} artwork`}
/>
) : (
<div className="spawn-card__img spawn-card__img--placeholder" aria-hidden="true">
🔥
</div>
)}
<div className="spawn-card__body">
<h2 className="spawn-card__name">
{s.spawnName}{" "}
<span className="spawn-card__ticker">${s.ticker}</span>
</h2>
{s.lore ? <p className="spawn-card__lore">{s.lore}</p> : null}
<dl className="spawn-card__meta">
{s.mint ? (
<div className="spawn-card__row">
<dt>Mint</dt>
<dd title={s.mint}>{truncate(s.mint)}</dd>
</div>
) : null}
<div className="spawn-card__row">
<dt>Status</dt>
<dd>{s.status}</dd>
</div>
</dl>
{s.pumpfunUrl ? (
<a
className="spawn-card__link"
href={s.pumpfunUrl}
target="_blank"
rel="noreferrer noopener"
>
View on Pump.fun
</a>
) : null}
</div>
</li>
))}
</ul>
)}
{failed ? (
<p className="spawn-page__note">
The Spawn record is resting. Try again shortly.
</p>
) : null}
</section>
<Footer />
</main>
);
}

View File

@@ -21,6 +21,7 @@ export function Footer() {
Repository
</a>
<a href="#scanner">Scanner</a>
<a href="/spawn">The Spawn</a>
</nav>
<p className="footer__disclaimer">