Core
The pegd_issuance Anchor program. Twelve instructions, seven account structs, one declare_id. Custody, mint authority, and circuit-breaker logic all live inside this single crate.
Pegd is a Solana-native issuance framework — a prototype vault that a protocol brings collateral to, from which it emits a calibrated stablecoin. Every layer is named, every account is sized, every hop from the issuer wizard to the on-chain mint is documented. This page is the system map: eight workspace packages, two deployed apps, one Anchor program, and the oracle plus attestor mesh that keeps the peg honest.
The framework separates the immutable core — an Anchor program that owns issuance, redemption, and attestation state — from the composable adapters that surround it. The core is small enough to audit line by line. Everything else is upgradeable, replaceable, or entirely optional.
The pegd_issuance Anchor program. Twelve instructions, seven account structs, one declare_id. Custody, mint authority, and circuit-breaker logic all live inside this single crate.
Attestor nodes, the FastAPI indexer, the Next.js control room, the SDK, and the CLI. Any of them can be swapped without touching the on-chain code — the program never trusts a TypeScript byte.
The monorepo contains eight packages and two apps, each pinned to a single responsibility. Package boundaries are enforced by the pnpm workspace graph — the SDK cannot import from the CLI, the CLI cannot reach into the Anchor crate.
| Package | Language | Responsibility |
|---|---|---|
| anchor-program | Rust | Issuance core, PoR commit, breaker |
| issuance-core | TypeScript | Client-side vault math and quoting |
| por-attestor | TypeScript | Signed reserve statements to chain |
| yield-engine | TypeScript | Token-2022 interest-bearing wiring |
| risk-module | TypeScript | Breaker thresholds, liquidation queue |
| compliance-hooks | Rust | Optional SPL transfer hook program |
| sdk-ts | TypeScript | Public @pegd/sdk facade |
| cli | TypeScript | pegd-cli npm binary |
| apps/web | Next.js 14 | Vault scene, Inspector, Studio, Registry |
| apps/service | FastAPI | Indexer, dashboard API, badge renderer |
The on-chain surface is one Anchor crate under packages/anchor-program/programs/pegd_issuance. Every PDA seed prefix is short, unambiguous, and encoded in the program constants. The IDL that ships to the SDK is generated from the exact same crate — there is no manual JSON to drift.
// Program-derived addresses. All seeds are lowercase ASCII.
config = pda([b"config"], program_id);
registry = pda([b"registry"], program_id);
stable = pda([b"stable", authority.as_ref()], program_id);
vault = pda([b"vault", stable_mint.as_ref()], program_id);
attestation = pda([b"attestation", stable_mint.as_ref()], program_id);
risk = pda([b"risk", stable_mint.as_ref()], program_id);
oracle = pda([b"oracle", stable_mint.as_ref()], program_id);
// Collateral vault token account = associated_token_address(
// owner = vault PDA,
// mint = collateral_mint,
// );Twelve handlers cover the full lifecycle. Each handler is a thin shell — it constructs a Context around the accounts it needs and delegates to a pure function in state/ so unit tests can drive the math without account plumbing.
| Handler | Effect |
|---|---|
| initialize_config | Creates Config singleton, sets admin |
| register_stable | Allocates mint with Token-2022 extensions |
| deposit_collateral | Moves collateral into the vault ATA |
| mint_stable | Emits units after ratio and cap checks |
| burn_stable | Retires supply, unlocks redemption |
| redeem_collateral | Burns then returns proportional collateral |
| commit_attestation | Verifies M-of-N signers, writes ratio |
| update_price | Reads Pyth PriceUpdateV2, rejects stale |
| pause_issuance | Manual breaker by admin authority |
| resume_issuance | Clears breaker after fresh attestation |
| set_risk_params | Updates RiskParams within config lock |
| set_yield_rate | CPI update_rate on interest-bearing mint |
Seven Anchor account structs. Every struct carries a _reserved: [u8; 32] pad so future fields can be added without a migration.
#[account]
pub struct VaultState {
pub stable_mint: Pubkey, // the Token-2022 mint we own authority for
pub collateral_vault: Pubkey, // ATA(vault, collateral_mint)
pub collateral_mint: Pubkey, // SPL or Token-2022
pub total_collateral: u64, // raw units at the collateral mint's decimals
pub total_minted: u64, // raw units at stable_mint decimals
pub collateral_ratio_bps: u32, // u32 avoids the 655.35% overflow on u16
pub last_update_ts: i64,
pub is_paused: bool,
pub bump: u8,
pub _reserved: [u8; 32],
}
impl VaultState {
pub const SPACE: usize = 8 + 32 * 3 + 8 * 3 + 4 + 1 + 1 + 32;
}The program consumes two independent data planes: a price feed from Pyth for the peg-currency exchange rate, and a set of reserve attestations from a bonded attestor mesh. Neither feed is ever trusted alone. Prices carry a confidence interval; reserves carry an M-of-N quorum. The median ratio wins.
PriceUpdateV2 account through the pull-based Solana receiver, rejecting on staleness beyond max_staleness_s or on conf / price wider than max_confidence_bps.The PoR path is a four-stage pipeline. Every stage is idempotent so a re-run at any point produces the same on-chain commit for the same inputs. This is what makes attestation replayable during audits.
// Stage 1 — collector: read raw balances and prices.
const collateralUnits = await connection.getTokenAccountBalance(vaultAta);
const priceUpdate = await hermes.getLatestPriceUpdates([feedId]);
// Stage 2 — aggregator: value the vault in the peg currency.
const collateralUsd = collateralUnits.value.uiAmount *
priceUpdate.parsed[0].price.price *
Math.pow(10, priceUpdate.parsed[0].price.expo);
const totalSupplyUsd = totalSupplyUi * pegPrice;
const ratioBps = Math.floor(collateralUsd / totalSupplyUsd * 10_000);
// Stage 3 — signer set: each attestor signs the same 186-byte body.
const body = borsh.serialize(attestationBodySchema, snapshot);
const signature = ed25519.sign(body, attestorSecret);
// Stage 4 — on-chain commit: bundle M ed25519 verifies + program call.
const tx = new Transaction()
.add(Ed25519Program.createInstructionWithPublicKey({ publicKey, message: body, signature }))
.add(program.methods.commitAttestation(payload).instruction());
await sendAndConfirm(connection, tx, signers);Every stablecoin registered through Pegd is a Token-2022 mint. Two extensions matter: interest-bearing (always attached, rate may be zero) and transfer hook (only when the issuer opts in to compliance hooks). Extension order is fixed at allocation time and cannot be added later.
use anchor_spl::token_2022::spl_token_2022::{
extension::ExtensionType, state::Mint,
};
let mut extensions = vec![ExtensionType::InterestBearingConfig];
if params.has_compliance_hook {
extensions.push(ExtensionType::TransferHook);
}
let mint_len = ExtensionType::try_calculate_account_len::<Mint>(&extensions)?;
system_program::create_account(
cpi_ctx,
Rent::get()?.minimum_balance(mint_len),
mint_len as u64,
&spl_token_2022::ID,
)?;
// Interest-bearing must initialize BEFORE initialize_mint2.
anchor_spl::token_2022_extensions::interest_bearing_mint_initialize(
ctx.accounts.into_interest_bearing_ctx(),
Some(config_pda),
params.yield_rate_bps,
)?;
if params.has_compliance_hook {
initialize_transfer_hook(&mint, compliance_hook_program_id)?;
}
// Only after all extensions are attached.
initialize_mint2(cpi_ctx, params.decimals, &vault_pda, None)?;/api/*. Route Handlers hold the Helius key and the Railway URL server-side. CORS is a non-event.pegd-web serves the vault scene, static docs, and the Route Handler proxy layer at /api/*. Helius keys live here as server-only env vars.
pegd-service hosts the indexer, reserve time-series, badge renderer, and the WebSocket fan-out. Postgres and Redis are managed add-ons.
pegd_issuance plus the optional compliance_hook program. Upgrades gated by Squads multisig with a 48-hour timelock.
| Variable | Surface | Notes |
|---|---|---|
| NEXT_PUBLIC_SOLANA_RPC | web (client) | Public mainnet-beta only. Never Helius. |
| HELIUS_RPC_URL | web (server), service | Keyed. Consumed by Route Handlers only. |
| CORS_ORIGINS | service | Four exact origins. No wildcard. |
| ANCHOR_PROGRAM_ID | service, web (server) | Set once after mainnet deploy. |
| DATABASE_URL | service | Managed Postgres injected by Railway. |