Built for the @stardotfun bounty “Permissionless Fee Routing for Meteora DAMM v2”, Vertex is a production-ready Solana module that automates daily quote-fee collection and distribution for DAMM v2 pools — no manual cranks, no creator intervention.
Vertex provides:
Opens a Meteora DLMM v2 liquidity position owned by a program PDA, guaranteed to accrue only quote-mint fees.
Anyone can trigger the crank once per UTC day to:
- Claim accrued quote fees from the Meteora pool
- Compute pro-rata investor payouts using Streamflow’s still-locked balances
- Route the remaining carry (creator share) to the creator’s quote ATA
Supports multi-page investor sets with deterministic continuation (no double-payouts, safe retries).
Reads on-chain vesting streams to compute real-time locked ratios for fair, transparent fee sharing.
We packaged the on-chain program, Rust/TypeScript SDKs, a CLI, and a reference Next.js UI so the Star team can drop it directly into their stack.
programs/fee_router Anchor program for honorary position + distribution crank
apps/ui-temp Reference Next.js UI (policy setup, honorary position, daily crank)
sdk/rust, sdk/ts Instruction builders and account helpers
scripts/ Local validator automation and fixtures
audits/ Threat model + security checklist
docs/ MkDocs documentation site
These steps produce the exact flow shown in the screenshots below. Everything runs on a local validator; Phantom (or any Solana wallet) must be set to Localnet and connected to localhost:3000.
# 1. Start a clean validator
solana-test-validator
# in a new shell: build + deploy the fee router program
anchor build -p keystone_fee_router
anchor deploy -p keystone_fee_router
# 2. Seed deterministic fixture accounts (quote mint, cp_pool, honorary position PDA seeds)
node scripts/setup-local-fixture.js
# 3. Launch the reference UI
npm --prefix apps/ui-temp run devEvery fixture run rewrites target/local-fixture.json and apps/ui-temp/app/config.ts with the program ID, Meteora pool, quote mint, PDAs, and ATAs that the UI pre-fills.
Where do I find the Program ID and IDL?
Both values are copied byscripts/setup-local-fixture.js. The script writes:
- Program ID:
LOCAL_FIXTURE.programId- Full IDL JSON:
Vertex/target/idl/keystone_fee_router.json- All PDAs / ATAs needed for the flow:
target/local-fixture.json
- Paste the Program ID and IDL JSON recorded in
target/local-fixture.json.
(The UI caches these inlocalStorageafter you press Save.) - Enter the Meteora
cp_poolfrom the same fixture file and press Fetch State.
Before the first crank runs you’ll see a policy summary plus a note indicating the Progress account will appear after the initial distribution.
All inputs are pre-populated from the fixture:
Meteora cp_pool–LOCAL_FIXTURE.cpPoolQuote mint–LOCAL_FIXTURE.quoteMintCreator quote ATA–LOCAL_FIXTURE.creatorAtaTreasury quote ATA–LOCAL_FIXTURE.treasuryAta- Economic knobs (
Y0,bps, cap, min payout)
Click Initialize Policy and approve the Phantom prompt. The transaction signature is displayed on success (e.g. Init Policy tx: ...). This stores the immutable configuration and the PDAs are displayed above the button for reference.
Again, values are filled automatically:
Policy PDA–LOCAL_FIXTURE.policyMeteora cp_pool–LOCAL_FIXTURE.cpPoolQuote mint–LOCAL_FIXTURE.quoteMintcp-position account–LOCAL_FIXTURE.cpPosition
Press Initialize Honorary Position to create the position PDA and tie it to the DLMM owner PDA. The UI echoes both the Honorary Position PDA and the fee-owner PDA derived from [VAULT_SEED, policy, investor_fee_pos_owner].
This form requires both static and per-investor values:
| Input | Default Source | Notes |
|---|---|---|
| Policy PDA | LOCAL_FIXTURE.policy |
Static |
| Meteora cp_pool | LOCAL_FIXTURE.cpPool |
Static |
| Treasury quote ATA | LOCAL_FIXTURE.treasuryAta |
Static, owned by the vault PDA |
| Creator quote ATA | LOCAL_FIXTURE.creatorAta |
Static, receives end-of-day remainder |
| Investor quote ATA | (must supply per investor page) | Streamflow-provided token account, same quote mint |
| Stream account | (must supply per investor page) | Streamflow stream PDA; fee router reads locked at crank time |
| Page cursor (u64) | Free-form | Caller-chosen opaque value for pagination bookkeeping |
| Carry cursor (u64) | Free-form | We reuse the fixture default 0; the crank stores it in the Progress account |
| “Is last page?” checkbox | Off by default | Toggle after you submit the final investor page of the UTC day (routes remainder to creator) |
For real payout pages you’ll pass the Streamflow stream and investor ATA for each investor. The UI accepts additional investor entries (ATA + stream) via the “Additional Investors” section (not shown in the screenshot) to cover multi-investor pages.
After the policy is initialized, Fetch State displays:
- Policy PDA and derived Progress PDA
- Immutable economic configuration
- Quote mint, Y0 total, fee share, caps, dust threshold
- Creator and treasury ATAs
Once you run the crank for the first time, the Progress card is populated with:
- Current UTC day (
floor(ts/86400)) - Last crank timestamp
- Claimed/distributed quote totals
- Carry-over, cursor, and
day_closedflag
| Requirement | Implementation Reference |
|---|---|
| Honorary position owned by PDA, quote-only fees | programs/fee_router/src/lib.rs::init_honorary_position (quote mint validation + Meteora CPI guard) |
| 24h distribution crank with pagination | programs/fee_router/src/lib.rs::crank_distribute |
| Idempotent daily tracking (Progress account) | Progress account schema (current_day, page_cursor, carry_quote_today, day_closed) |
| Per-investor pro-rata math with caps & dust | compute_investor_quote and distribution loop |
Remainder routed to creator when is_last_page |
Final branch in crank_distribute |
| Quote-only guard (base fees rejected) | assert_cp_pool_quote_only + Meteora CPI result checks |
| Streamflow integration | stream_adapter::StreamLockedReader (mock adapter) |
| Events emitted | PolicyInitialized, HonoraryPositionInitialized, InvestorPayoutPage, CreatorPayoutDayClosed |
Tests and scripts that exercise the happy path live under tests/, scripts/, and the Next.js UI.
- All PDAs store bumps on-chain to prevent accidental drift.
- Honorary position initialization validates pool mint ordering and quote-only accrual.
- Crank enforces 24h gating, handles retries safely, and never double-pays.
- Arithmetic uses checked 128-bit intermediates to avoid overflow.
See audits/threat_model.md and audits/checklist.md before mainnet deployment.
-
Rust + TypeScript SDKs: instruction builders and account parsers for integrating the module into Star’s backend.
-
CLI (
cli/): operational commands for deployments and configuration. -
Docs (MkDocs) can be served locally:
pip install mkdocs-material mkdocs serve
Star can integrate Vertex to manage their token sale fee flows on-chain, ensuring transparent, automatic investor revenue distribution via Meteora liquidity pools. 🚀




