Monetization (design Rev 4, §3.1) — transparent in-tx fee, non-custodial:
- @pyre/core: computeFeeBreakdown (single source of truth, BigInt) + FeeBreakdown
threaded through close/burn previews; fee tests.
- @pyre/config: PYRE_TREASURY_WALLET / PYRE_FEE_BPS (500) / swap fee / max contribution.
- @pyre/solana: close-empty + burn→close now append ONE System transfer of exactly
the disclosed fee to the treasury; rent/authority/feePayer pinned to wallet.
buildBurnTx re-validates EVERY account on-chain and value-gates via the classifier
(classic SPL + Token-2022) — never burns protected/valuable/NFT/unsupported;
ignores client amount (burns real balance); whole-build rejection.
- @pyre/api: close-empty/burn endpoints carry the fee + bounded optional contribution;
/api/receipt persists (cleanup_receipts) and records the on-chain treasury fee as
Essence; GET /api/essence; startup migrate(). Best-effort DB (never fails receipts).
- @pyre/db: Postgres Essence ledger (rounds, cleanup_receipts, essence_contributions),
idempotent migrations, parameterized + u64-safe.
- @pyre/web: fee preview ("reclaim · feeds the PYRE · you net" + treasury) + optional
"feed more" slider; burn flow w/ destructive confirm; decode+match verifies the fee
transfer (treasury + exact lamports) before signing; public "🔥 fed the PYRE" panel.
Built by agents (2 waves) + 2 audits. Security audit found a HIGH — buildBurnTx
didn't value-gate CLASSIC spl tokens (a direct API caller could burn USDC/an NFT);
FIXED (classify classic accounts too) + 2 regression tests. Integration: SHIP.
typecheck 8/8, core 91, solana 30, web build green. Live: burn preview on the dust
token shows 5% → treasury; non-empty/non-owned/valuable rejected. Nightly DB backup
cron enabled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pyre/db
Postgres-backed Essence ledger for PYRE. A small typed data layer over
pg (no ORM): a lazily-created connection pool, an idempotent migration
runner, and the round / receipt / contribution query surface.
Trust rules
- Connection details come from the environment (
DATABASE_URL) or an explicit argument — credentials are never hardcoded. The localhost dev URL is only a last-resort fallback. - Recovered ATA rent is not Essence.
cleanup_receiptsrecords rent returned to the user; it never touches a round total. Onlyessence_contributions(the protocol fee and explicit opt-in contributions) feedrounds.essence_lamports. - Parameterized queries only (
$1,$2, …) — no string interpolation. - Lamport amounts cross the API boundary as decimal strings (u64-safe) and
are cast to
::bigintin SQL. - No network/DB access at import time. The pool is created lazily;
migrate()is safe to call repeatedly.
Tables
Defined in migrations/001_init.sql (idempotent, CREATE TABLE IF NOT EXISTS).
rounds
| column | type | notes |
|---|---|---|
id |
BIGSERIAL |
primary key |
status |
TEXT |
'open' | 'closed', default 'open' |
essence_lamports |
BIGINT |
running round total, default 0 |
started_at |
TIMESTAMPTZ |
default now() |
closed_at |
TIMESTAMPTZ |
nullable |
cleanup_receipts
| column | type | notes |
|---|---|---|
id |
BIGSERIAL |
primary key |
wallet |
TEXT |
|
tx_signature |
TEXT |
unique (idempotency key) |
kind |
TEXT |
'close' | 'burn' |
rent_returned_lamports |
BIGINT |
rent returned to the user |
fee_lamports |
BIGINT |
protocol fee, default 0 |
closed_accounts |
JSONB |
array of addresses, default '[]' |
created_at |
TIMESTAMPTZ |
default now() |
Index: cleanup_receipts(wallet).
essence_contributions
| column | type | notes |
|---|---|---|
id |
BIGSERIAL |
primary key |
round_id |
BIGINT |
FK → rounds(id) |
wallet |
TEXT |
|
tx_signature |
TEXT |
unique (idempotency key) |
lamports |
BIGINT |
amount fed to the PYRE |
kind |
TEXT |
'fee' | 'contribution' |
created_at |
TIMESTAMPTZ |
default now() |
Index: essence_contributions(round_id).
API
import {
getPool,
migrate,
ensureOpenRound,
recordReceipt,
recordEssence,
getEssenceSummary,
closePool,
} from "@pyre/db";
getPool(databaseUrl?): Pool— lazily create and cache the singletonpg.Pool. Connection string resolves to the explicit argument, thenDATABASE_URL, then the localhost dev default. No connection is opened until first query.migrate(): Promise<void>— apply everymigrations/*.sqlin name order, each in its own transaction. Idempotent; safe to call repeatedly.ensureOpenRound(): Promise<{ id: string }>— return the current open round, creating one if none exists.recordReceipt(r): Promise<void>— insert a cleanup receipt ({ wallet, txSignature, kind: 'close'|'burn', rentReturnedLamports, feeLamports, closedAccounts }). Idempotent ontxSignature. Does not affect any round total.recordEssence(e): Promise<{ recorded, roundId }>— record a contribution ({ wallet, txSignature, lamports, kind: 'fee'|'contribution' }) against the open round. In one transaction: ensures a round, inserts (idempotent ontxSignature), and incrementsrounds.essence_lamportsonly when a new row is inserted. Returnsrecorded: falsefor duplicate signatures.getEssenceSummary(): Promise<EssenceSummary>— open-round{ roundId, totalLamports, contributionCount, recent }, whererecentis the last ~10 contributions (newest first).closePool(): Promise<void>— close and clear the pool for shutdown / test teardown.
All lamport amounts are strings in and out.
Usage
await migrate();
await recordEssence({
wallet: "Wallet…",
txSignature: "Sig…",
lamports: "1000000",
kind: "fee",
});
const summary = await getEssenceSummary();
Migrations
SQL lives in migrations/, one forward migration per file in lexical order
(001_init.sql, 002_…sql, …). Each file must be idempotent. The runner
(migrate()) applies them in name order against the resolved connection.