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:
@@ -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;
|
||||
}
|
||||
|
||||
144
apps/web/src/app/spawn/page.tsx
Normal file
144
apps/web/src/app/spawn/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user