The on-chain side of Offhrs: why PreStocks need a wrapper, how the dividend vault pays holders, and where every account and instruction lives.
Two problems, one Anchor program, and the reason the wrapper exists.
Offhrs is an autonomous marketplace where AI trading agents arbitrage tokenized pre-IPO equity after the regular market closes, and stream the proceeds to the people holding each agent's token — paid in the wrapped share itself, not in a stablecoin.
The on-chain half is a single Anchor program, `stock_vault`. It solves two problems that have nothing to do with each other except that both stand between the thesis and a working product.
The reference market closes, the tokenized marks keep trading, and that gap is the product.
| Surface | Where |
|---|---|
| Program | programs/stock_vault — wrapper · registry · vault · Pyth · execution log |
| Off-chain agent | agent/src — config · market · signal · execution · chain |
| Frontend | web/ — Next.js App Router, server actions for every chain read and write |
Why a liquid token trades away from a frozen reference price.
The official value of a tokenized share comes from Pyth. When the underlying exchange closes, the equity feed's publish time stops advancing — the price does not move because there is no new price. The tokenized share keeps trading on Solana all night, so its mark drifts from the stale reference.
That drift is the tradable object. An agent buys the token when it trades below the last reference and sells when it trades above, and only when the gap clears its costs. The result is a measurable basis, not a narrative: the basis is the difference between the DEX price and the reference mark, in basis points.
The program does not ask an API whether the market is open. record_signal reads a Pyth PriceUpdateV2 account and derives MarketRegime::{Live, Frozen} from whether publish_time advanced within the caller's policy. If Pyth were the only source, a Pyth outage would look like an open market — so the session clock is computed from the exchange calendar separately.
The Token-2022 extensions that make the raw mint unusable, and why.
A PreStock is a Token-2022 mint with the extensions a compliant security token needs: a transfer-fee configuration (50bps at launch, stepping up later), a permanent delegate, a pausable configuration and a scaled-UI-amount multiplier. They are exactly what makes the token usable as a regulated share — and exactly what makes it unusable as a DBC quote mint.
| Extension | Consequence for the protocol |
|---|---|
| TransferFeeConfig | DBC rejects the mint as a quote asset (6081). The wrapper pays the fee once, on the way in. |
| PermanentDelegate | The issuer can move a holder's tokens. The wrapper's backing can be seized; see the invariant caveat below. |
| PausableConfig | PreStock transfers can halt from under us, which is why the wrapper has its own set_paused. |
| ScaledUiAmount | A display multiplier that changes on corporate actions. The wrapper is 1:1 at the raw amount level, so it is transparent to it. |
wrapped supply == reserve balance holds against every instruction this program runs. It can still be broken by the issuer exercising the permanent delegate on the PreStock mint and moving the reserve out. That is bad debt by construction, and it is disclosed rather than hidden.
The words this codebase uses, defined once.
| Term | Meaning |
|---|---|
| PreStock | A Token-2022 tokenized stock. Transfer-fee bearing, with a permanent delegate and a pausable config. |
| wPreStock | The zero-fee classic-SPL wrapper of a PreStock. 1:1 at the raw amount level; the DBC quote asset. |
| $AGENT | An agent's own SPL token, created on its bonding curve. The token holders stake. |
| Wrapper | The config + reserve pair behind one wPreStock. Holds the raw PreStock backing the wrapped supply. |
| Dividend vault | The per-agent vault that stakes `$AGENT` and streams wPreStock to holders. |
| Accumulator | acc_reward_per_share — the fixed-point figure that turns a stream into each holder's share. |
| Basis | The gap between the DEX price and the Pyth reference mark, in basis points. |
| Regime | Live or Frozen — whether the reference feed's publish time is still advancing. |
| DBC | Meteora's Dynamic Bonding Curve — the curve `$AGENT` trades on before migration. |
| Migration | The point the curve graduates to a DAMM v2 pool. Measured in quote-token units. |
| Execution | One logged arbitrage, Pyth-attested. Its oracle fields are copied from a Signal on chain. |
A zero-fee, 1:1-backed classic-SPL twin of each PreStock.
initialize_wrapper creates two accounts per PreStock: a classic SPL mint whose mint authority is the WrapperConfig PDA, and a reserve token account that holds the raw PreStock backing it. The wrapped mint is the DBC quote asset — the only reason a stock-paired curve is possible.
wrap measures the reserve delta rather than trusting the requested amount. A raw PreStock transfer arrives net of its fee, so the program reads the reserve balance before and after and mints exactly what arrived. Minting the request would create unbacked supply on every wrap.
wrapped_mint.supply == reserve.amount
// wrap: delta = reserve.after - reserve.before → mint delta
// unwrap: burn amount → transfer amount, fee paid here| Instruction | Effect |
|---|---|
| initialize_wrapper | Create the wrapped mint + reserve for one PreStock mint. |
| wrap(amount) | Deposit raw PreStock; receive wPreStock 1:1 on the received amount. |
| unwrap(amount) | Burn wPreStock; receive raw PreStock less the PreStock transfer fee. |
| set_paused(paused) | Admin circuit breaker, mirroring the PreStock pausable config. |
The config carries four transparency counters — total_requested_in, total_received_in, total_fee_paid_in and total_unwrapped — so the fee actually paid on the way in is visible on chain, not estimated in a UI.
Rewards stream per slot, so time held is what pays.
Every agent has one DividendVault. Holders stake the agent's `$AGENT` token into it and earn wrapped PreStock. The vault is the Synthetix staking-rewards pattern: deposits set a reward rate over a duration, and the per-share accumulator is advanced by however many slots have elapsed since it was last touched.
PRECISION = 1e12
payout = min(reward_rate * elapsed, reward_reserve) // capped at what it holds
increment = payout * PRECISION / total_staked
acc_reward_per_share += increment
reward_reserve -= payout
// what a holder can claim:
pending = staked * acc_reward_per_share / PRECISION - reward_debt| Instruction | Who | Effect |
|---|---|---|
| initialize_vault(min_hold_slots) | creator | Create the stake + reward PDAs for a registered agent. |
| stake(amount) | holder | Move `$AGENT` in; rewards start streaming from this slot. |
| unstake(amount) | holder | Withdraw `$AGENT`; accrued rewards stay claimable. |
| claim() | holder | Transfer accrued wPreStock out of the reward vault. |
| deposit_rewards(amount, duration_slots) | creator or signer | Fund a stream. Fees and arbitrage profit route in here. |
One account per agent, joining a token, a share and a vault.
An Agent account binds the creator, the execution signer (the Clawpump bot's keypair), the `$AGENT` mint, the wrapped PreStock it pays out in, and the vault address. register_agent is permissionless — anyone can launch an agent — and the account is addressed by the agent's own mint, so the mint is the identity.
| Field | Meaning |
|---|---|
| creator | Deploys the agent, sets the fee, receives creator royalties. |
| agent_signer | Execution keypair; may route profits into the vault. |
| agent_token_mint | The DBC-created `$AGENT` mint — the staking token. |
| wrapped_mint | The reward token. Always the wrapper, never raw PreStock. |
| dynamic_fee_bps | DBC dynamic fee tier, 500–1500 bps at launch. |
| execution_count | Logged executions; also the PDA index counter. |
close_agent returns the registration's rent, but refuses while the vault still has stakers. Deregistering can never strand someone's rewards.
Meteora DBC, quoted in wPreStock, with the fee accruing in the share.
The `$AGENT` token trades on a Meteora Dynamic Bonding Curve. The config is created by the app when Clawpump is unavailable, and the one setting that matters for the product is the quote asset: the pool is quoted in wPreStock, and fees are collected in the quote token. That is what makes “dividends paid in equity” literally true rather than a marketing line.
quoteMint: wPreStock // the zero-fee wrapper
collectFeeMode: QuoteToken // fees accrue in the share
creatorTradingFeePercentage: 0 // the creator does not skim the vault
migrationOption: MET_DAMM_V2 // graduates to a DAMM v2 pool
migrationQuoteThreshold: 100 wPreStockA logged execution cannot exist without a real oracle read behind it.
record_signal reads a Pyth PriceUpdateV2 account on chain — permissionless, no API key — checks that it is owned by the Pyth receiver, that it carries the expected feed id, and that it is inside the caller's staleness policy, then writes a Signal. The regime is derived from publish_time: a feed that has stopped advancing is Frozen, which is the thesis encoded as an enum.
log_arb then copies the oracle fields from the referenced Signal rather than accepting them from the caller. The Pyth price, exponent, publish time and staleness on an ArbExecution are therefore attestation, not assertion — which is what makes the execution log evidence.
ArbExecution {
agent, index, venue,
amount_in, amount_out,
profit, // amount_out.saturating_sub(amount_in), on chain
oracle, feed_id, pyth_price, pyth_exponent,
pyth_publish_time, pyth_staleness_secs,
regime, executed_at,
}Every account the program owns, and the seeds that address it.
The frontend does not carry an Anchor client. It decodes these accounts with small explicit Borsh readers that assert the 8-byte discriminator first, so a layout change fails loudly rather than decoding garbage.
| Account | Seeds | Holds |
|---|---|---|
| WrapperConfig | ["wrapper", prestock_mint] | wrapped mint, reserve, admin, pause, fee counters |
| Reserve | ["reserve", wrapper_config] | the raw PreStock backing the wrapper |
| Agent | ["agent", agent_token_mint] | creator, signer, fee, counters, vault |
| DividendVault | ["vault", agent_token_mint] | stake + reward mints, accumulator, stream |
| StakeVault | ["stake_vault", vault] | staked `$AGENT` |
| RewardVault | ["reward_vault", vault] | wPreStock waiting to stream |
| UserStake | ["stake", vault, owner] | one holder's stake, debt and accrual |
| Signal | ["signal", agent, feed_id] | the latest Pyth-attested market read |
| ArbExecution | ["exec", agent, index] | one Pyth-attested execution |
Where value accrues, and the constants that define it.
An agent token trades on a bonding curve. The trading fee is charged in the quote asset — wPreStock — and that fee is the vault's income: it streams to stakers as the share itself. The creator's own trading-fee percentage is zero, so the vault is not competing with the creator for the same fees.
| Constant | Value | Why |
|---|---|---|
| `$AGENT` supply | 1,000,000,000 (6 decimals) | Fixed at launch; the curve sells the supply and migrates the rest. |
| Quote decimals | 9 | wPreStock inherits the PreStock's 9. |
| Curve fee | 5–15% at launch | A linear schedule decaying to 1% over 24 hours — the agent's main income. |
| Creator trading fee | 0% | Fees accrue to the vault, not the creator. |
| Migration | DAMM v2 | The curve graduates to a full DAMM v2 pool at the quote threshold. |
A holder earns from the stream regardless of the token's price. Betting on the agent's trading is separate from collecting the share it routes — the vault pays for time held, not for being right.
What is live where, and the one spend still outstanding.
| Thing | Value |
|---|---|
| stock_vault program | FoVBZRFCamH1HNMiVpNZV2QJxk9bSxWtQvKgmqZ1rVLw |
| Meteora DBC program | dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN |
| Program cluster | devnet |
| Market data | mainnet — PreStocks, Jupiter and Pyth equity feeds exist only there |
The program runs on devnet, where the whole loop works. The market inputs are read from mainnet and the program writes land on devnet.
Real PreStocks only exist on mainnet, so on devnet the wrappers point at mock Token-2022 mints carrying the same extensions. They borrow the identity of the real shares they stand in for, so a card, a launch row and a terminal all read the same figures.
Build, test, and drive the agent on macOS, Linux or Windows.
Anchor, the Solana CLI and the Meteora SDK are Linux-first. On Windows, install WSL2 with an Ubuntu distro and run every command inside it rather than in PowerShell — the local validator and the toolchain assume a Unix filesystem.
# Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Solana CLI (Agave)
sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
# Anchor, through avm
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
avm install latest && avm use latest
# Node 20+ and pnpm
corepack enable && corepack prepare pnpm@latest --activateanchor build
anchor test # 30 integration tests
cargo test --manifest-path programs/stock_vault/Cargo.toml # 15 unit testscd web
pnpm exec next dev -p 3939 # dev server
pnpm exec next build && pnpm exec next start -p 3939ANGEL_ONCE=1 pnpm exec tsx agent/src/index.ts # one read-only pass
ANGEL_SYMBOL=OPENAI pnpm exec tsx agent/src/index.ts # a different asset
pnpm exec tsx agent/src/index.ts --feeds # known on-chain feedsThe check scripts are the fastest way to verify a subsystem end to end: position-check for the vault maths, trade-check for buy/auto-stake/sell/wrap/unwrap on devnet, and browser-sign-check for a real Wallet Standard signature through the UI.
The server never holds a key that can move your funds.
The program is covered by a test suite today — 15 unit and 30 integration tests, including an independent model of the reward accumulator — and an independent audit is being arranged before the product opens to real value. Until that lands, treat this deployment as what it is: a devnet beta.
The honest list, kept where the technicals live.
What the suite actually covers, and how to reproduce a result.
anchor test # 30 integration tests
cargo test --manifest-path programs/stock_vault/Cargo.toml # 15 unit testsThe app ships its own checks, which are the honest way to verify a subsystem against a live cluster rather than a mock: position-check (the vault maths), trade-check (buy/auto-stake/sell/wrap/unwrap), launch-curve-check (a real DBC config and pool), and browser-sign-check (a real Wallet Standard signature through the UI).
The failures that look like something else.
The buy path spends the quote token — wPreStock — not SOL. If the wallet holds none for the asset you picked, the SPL token program returns InsufficientFunds, and an older build reported that as a SOL problem. Check the wPreStock balance first, and make sure the wallet is on the same cluster as the program.