Off-Chain Services
✅ Ready to Dev
These are off-chain service specifications, not smart contracts. A developer can implement from this document.
Not smart contracts. No PDAs, no on-chain state, no program deployment. Each bot is a server-side process with a dedicated wallet. All wallets are registered on-chain with specific roles.
Service Overview
| Bot | Contract | Instruction | Frequency | Wallet Role |
|---|---|---|---|---|
| Pool Rebalancer | Native DEX | grow_liquidity / compress_liquidity | Every 60s (if deviation > 1%) | dex_config.rebalancer |
| Merkle Publisher | Yield Distribution | publish_root | Every 10 min | config.publish_authority |
| Revenue Crank | Ownership Token | distribute_revenue | Every hour (if conditions met) | Permissionless |
| Convert & Fund Crank | Yield Distribution | convert_to_rwt | After each revenue distribution | Permissionless |
| Yield Claim Crank | RWT Engine + DEX + OT | claim_yield + compound_yield + claim_yd_for_treasury | Every 30 min | Permissionless |
| Nexus Manager | Native DEX | nexus_swap, nexus_add/remove_liquidity | Strategy-based | LiquidityNexus.manager |
Pool Rebalancer
Maintains the Monotonic Ladder geometry of master pools (RWT/USDC, RWT/USDY). Reads on-chain NAV from RWT Engine and — depending on direction of NAV movement — callsgrow_liquidity (rising NAV) or compress_liquidity (writedown). Does not touch StandardCurve pools.
How It Works
Read Pool State
For each master pool: query
pool_state.last_rebalance_nav_bin, pool_state.active_zone_lower, and bin_array. Compute current reference price: price = (1 + bin_step_bps/10000) ^ last_rebalance_nav_bin.Check Deviation
deviation = (nav_price − ref_price) / ref_price (signed). If abs(deviation) > REBALANCE_THRESHOLD → rebalance needed.Route: Growth vs Compression
- If
new_nav_bin > pool_state.last_rebalance_nav_bin→ growth path. Callnative_dex::grow_liquidity(new_nav_bin, ACTIVE_ZONE_WIDTH). DEX pulls USDC from Nexus accumulator, extends active bid wall right, redistributes geometric density around new NAV. Permanent tail untouched. Organic ask untouched. - If
new_nav_bin < pool_state.last_rebalance_nav_bin(governance writedown) → compression path. Callnative_dex::compress_liquidity(new_nav_bin, ACTIVE_ZONE_WIDTH). DEX recenters density on lower NAV. Former active zone above new NAV becomes extended bid. Organic ask above new NAV freezes in place for future recovery.
Configuration
| Parameter | Default | Description |
|---|---|---|
REBALANCE_THRESHOLD | 0.01 (1%) | Min absolute NAV deviation to trigger rebalance |
ACTIVE_ZONE_WIDTH | 40 | Bins in the geometric active bid wall |
GEOMETRIC_R_BPS | 8500 | Geometric density ratio (r = 0.85). Controls how tightly capital concentrates at the active bin |
CHECK_INTERVAL_SECS | 60 | How often to check deviation |
On-Chain Interaction
| Action | Instruction | Accounts |
|---|---|---|
| Read NAV | — | rwt_vault (read) |
| Read bins | — | pool_state, bin_array (read) |
| Grow | native_dex::grow_liquidity | rebalancer (signer), dex_config, pool_state (mut), bin_array (mut), liquidity_nexus (mut), nexus_usdc_ata (mut), pool_vault_b (mut), token_program |
| Compress | native_dex::compress_liquidity | rebalancer (signer), dex_config, pool_state (mut), bin_array (mut) |
Permissions
- CAN: call
grow_liquidity/compress_liquidityon master pools - CANNOT: swap, add/remove liquidity, change config, pause pools, extract funds
- If compromised: at worst can trigger redundant rebalances (compute waste) or pick suboptimal
new_nav_binwithin ±1 bin rounding. Cannot drain pool or Nexus — bothgrow_liquidityandcompress_liquidityconserve capital by design. Team replaces viaupdate_dex_config(rebalancer: new_wallet). - Emergency kill-switch: Authority calls
update_dex_config(rebalancer = [0u8;32], ...). This immediately freezesgrow_liquidity/compress_liquidityon-chain. The bot does not need to be stopped — its CPI calls will revert withInvalidRebalancer. Restore by settingrebalancerback to a real wallet.
Skip Conditions
- Pool is paused (
pool_state.is_active == false) - Pool is not a Monotonic Ladder pool (StandardCurve pools need no rebalancing)
- Deviation below threshold
- Nexus accumulator empty AND growth path (waits for next Nexus deposit before growing)
- Last rebalance < 60 seconds ago (debounce)
Merkle Publisher
Builds merkle trees of OT holder weights off-chain and publishes roots on-chain. This is the core of the yield distribution system — without published roots, holders cannot claim.How It Works
Scan Holders
For each OT project: call
getParsedProgramAccounts to get all token accounts for the OT mint. Includes regular wallets, RWT Vault PDA (holds OT as backing), DEX pool PDAs (hold OT in reserves), and OT Treasury PDA. Filter to holders with ≥ $100 total protocol holdings (OT + RWT combined).Calculate Weights
For each eligible holder (including PDAs):Non-eligible holders’ share → ARL OtTreasury PDA
["ot_treasury", arl_ot_mint] as a leaf in the tree (protocol revenue). Note: RWT Vault PDA, DEX pool PDAs are regular OT holders in the tree — they claim via their respective CPI instructions (claim_yield, compound_yield). ARL Treasury claims via claim_yd_for_treasury on the ARL OT instance.Build Tree
Construct merkle tree. Leaf format:
sha256(claimant_pubkey_bytes || cumulative_amount_le_bytes).Publish Root
Call
yield_distribution::publish_root(merkle_root, max_total_claim). Server wallet signs as publish authority.Configuration
| Parameter | Default | Description |
|---|---|---|
PUBLISH_INTERVAL_SECS | 600 (10 min) | How often to rebuild and publish |
MIN_HOLDING_USD | 100 | Minimum holding to be eligible ($100) |
PROOF_STORAGE | — | Path or DB where proofs are stored for claim UI |
On-Chain Interaction
| Action | Instruction | Accounts |
|---|---|---|
| Read holders | — | OT token accounts (read via RPC) |
| Read distributor | — | merkle_distributor (read) |
| Publish | yield_distribution::publish_root | publish_authority (signer), config, distributor (mut) |
Permissions
- CAN: call
publish_rooton any distributor - CANNOT: fund, claim, close distributors, update config
- If compromised: attacker can publish fraudulent roots → drain reward vault via fake claims. Team immediately replaces via
update_publish_authority. All publishes visible inRootPublishedevents.
Cost
~0.0006/TX).Revenue Crank
Triggers OT revenue distribution when conditions are met. Checks each OT project’s RevenueAccount balance and callsdistribute_revenue if the balance is above minimum and cooldown has passed.
How It Works
Check Balance
For each OT project: read RevenueAccount ATA balance. If
balance < min_distribution_amount ($100), skip.Check Cooldown
Read
revenue_config.last_distribution_ts. If now - last_distribution_ts < 604,800 (7 days), skip.Distribute
Call
ownership_token::distribute_revenue. Permissionless — any wallet can call.- Protocol fee deducted first: 0.25% (25 bps) of total balance →
areal_fee_destination(Areal Finance USDC ATA) - Remainder split proportionally to configured destinations (default):
- 70% (7,000 bps) → YD Accumulator (USDC)
- 20% (2,000 bps) → OT Treasury
- 10% (1,000 bps) → Crank USDC ATA → Nexus via
nexus_deposit
Configuration
| Parameter | Default | Description |
|---|---|---|
CHECK_INTERVAL_SECS | 3600 (1 hour) | How often to check each OT project |
OT_PROJECTS | — | List of OT mint addresses to monitor |
On-Chain Interaction
| Action | Instruction | Accounts |
|---|---|---|
| Read balance | — | RevenueAccount ATA (read) |
| Read cooldown | — | revenue_config (read) |
| Distribute | ownership_token::distribute_revenue | crank (signer), ot_config, revenue_config (mut), revenue_ata (mut), destination ATAs (mut) |
Permissions
- Permissionless — no special wallet role needed. Any wallet can trigger distribution.
- If compromised: no risk — crank only triggers distribution to pre-configured on-chain destinations. Cannot change destinations or extract funds.
Convert & Fund Crank
Converts accumulated USDC in YD Accumulator PDAs to RWT and deposits into distributor reward vaults. Should run immediately after each revenue distribution to minimize time between USDC arrival and RWT availability for claims.How It Works
Convert
Call
yield_distribution::convert_to_rwt(max_swap_amount). Atomic instruction that:- Swaps USDC → RWT on DEX (up to NAV price)
- Mints remainder at NAV via RWT Engine
- Deducts 0.25% protocol fee in RWT
- Deposits net RWT into reward vault
- Updates vesting state (locks previously vested, starts new vesting)
Configuration
| Parameter | Default | Description |
|---|---|---|
CHECK_INTERVAL_SECS | 300 (5 min) | How often to check accumulators |
MAX_SWAP_RATIO | 0.5 | Max fraction of USDC to swap on DEX (rest minted at NAV) |
On-Chain Interaction
| Action | Instruction | Accounts |
|---|---|---|
| Read balance | — | Accumulator USDC ATA (read) |
| Convert + fund | yield_distribution::convert_to_rwt | crank (signer), config, distributor (mut), accumulator, ATAs, DEX accounts, RWT Engine accounts |
Permissions
- Permissionless — any wallet can call. Accumulator PDA signs all internal transfers.
- If compromised: no risk — instruction only converts USDC already in Accumulator to RWT in reward vault. Cannot redirect funds.
Trigger
Ideally triggered immediately after Revenue Crank distributes USDC to an Accumulator. Can poll on interval as fallback.Yield Claim Crank
Claims RWT yield from YD merkle streams on behalf of three types of on-chain PDAs:- RWT Vault PDA — holds OT positions, earns yield as OT holder. Claimed RWT split: 70% NAV / 15% Nexus / 15% ARL Treasury.
- DEX Pool PDAs — OT/RWT pools hold OT in reserves, earning yield. Claimed RWT auto-compounds into pool reserves, benefiting all LP holders.
- OT Treasury PDAs — non-eligible holders’ (< $100) yield share is allocated to OT Treasury PDA in the merkle tree. Claimed RWT stays in treasury.
How It Works
Get Proofs
Read the latest merkle proofs from the Merkle Publisher’s proof storage for:
- RWT Vault PDA (one per protocol)
- Each OT/RWT pool PDA that holds OT (one per pool)
Claim for RWT Vault
Call
rwt_engine::claim_yield(cumulative_amount, proof). Permissionless instruction that:- CPIs to
yield_distribution::claimwith vault PDA as claimant - Splits claimed RWT: 70% stays in vault (NAV growth), 15% → Yield Distribution
LiquidityHoldingPDA RWT ATA, 15% → ARL Treasury - Updates
total_invested_capitaland recalculates NAV - Authority subsequently drains the
LiquidityHoldingslice into the Liquidity Nexus viayield_distribution::withdraw_liquidity_holding(single TX, atomic principal-floor update via CPI tonative_dex::nexus_record_deposit). The crank does not handle the RWT lane — it is Authority-gated
Compound for DEX Pools
For each OT/RWT pool: call
native_dex::compound_yield(cumulative_amount, proof). Permissionless instruction that:- CPIs to
yield_distribution::claimwith pool PDA as claimant - Claimed RWT added directly to pool reserves (auto-compound)
- All LP holders benefit proportionally — no individual claim needed
Claim for ARL Treasury (non-eligible yield)
Call
ownership_token::claim_yd_for_treasury(cumulative_amount, proof) on the ARL OT instance. This claims non-eligible holders’ yield from ALL OT projects as protocol revenue:- CPIs to
yield_distribution::claimwith ARL OtTreasury PDA as claimant - Claimed RWT arrives in ARL Treasury’s RWT ATA
- RWT is protocol revenue — spendable by ARL Futarchy governance via
spend_treasury
Configuration
| Parameter | Default | Description |
|---|---|---|
CLAIM_INTERVAL_SECS | 1800 (30 min) | How often to attempt claims |
PROOF_SOURCE | — | Merkle Publisher’s proof storage (DB or API) |
OT_RWT_POOLS | — | List of OT/RWT pool addresses to compound |
SEND_TX | false | Dry-run flag. true = live submit; false = compute and log decisions only. Flipped to true after the staging-mode run is verified. |
On-Chain Interaction
| Action | Instruction | Accounts |
|---|---|---|
| Read proofs | — | Off-chain: Merkle Publisher proof storage |
| Claim for vault | rwt_engine::claim_yield | crank (signer, mut), rwt_vault (mut), dist_config, RWT ATAs, YD CPI accounts |
| Compound for pool | native_dex::compound_yield | crank (signer, mut), pool_state (mut), target_vault (mut), YD CPI accounts |
| Claim for treasury | ownership_token::claim_yd_for_treasury | crank (signer, mut), ot_treasury, treasury_rwt_ata, YD CPI accounts |
| Route USDC to Nexus | native_dex::nexus_deposit | crank (signer), nexus (mut), token accounts. USDC lane only — the RWT lane is Authority-gated via withdraw_liquidity_holding and is not driven by this crank. |
Permissions
- Permissionless — any wallet can call all three instructions. PDAs sign their own CPI claims internally.
- If compromised: no risk — claimed RWT goes to pre-configured on-chain destinations. Cannot change split ratios, destinations, or redirect pool compound.
Dependency
Requires Merkle Publisher to have published a root that includes RWT Vault PDA and pool PDAs as claimants. If no valid proof exists, claim will fail withInvalidProof.
Nexus Manager
Manages the Liquidity Nexus PDA’s positions in DEX pools. The Nexus holds protocol-owned liquidity (funded by 10% of OT revenue + 15% of RWT yield). The manager bot decides when and where to deploy this capital. Principal is permanently locked — only LP profits (swap fees, auto-compound) can be withdrawn to Areal Treasury by authority.How It Works
Deposit Capital
Two lanes route capital into the Nexus with principal tracking — neither is driven by the Nexus Manager itself, but both feed the capital that the Manager subsequently deploys:
- USDC lane. OT
distribute_revenuesends 10% USDC to a crank wallet. The crank callsnative_dex::nexus_depositto route the USDC into the Nexus and bumptotal_deposited_usdc. - RWT lane. RWT
claim_yieldsends the 15% liquidity slice into the Yield DistributionLiquidityHoldingPDA’s RWT ATA. Authority then callsyield_distribution::withdraw_liquidity_holding, which atomically transfers RWT fromLiquidityHoldinginto the Nexus RWT ATA AND CPIs intonative_dex::nexus_record_depositto updatetotal_deposited_rwt— single TX.
Evaluate Pools
Analyze DEX pools: TVL, volume, spread, utilization. Identify pools that need more liquidity.
Deploy Liquidity
Call
native_dex::nexus_add_liquidity to LP into target pools. Can also call nexus_swap to rebalance between tokens before adding.Rebalance
Periodically rebalance positions:
nexus_remove_liquidity from underperforming pools, nexus_add_liquidity to better pools.Configuration
| Parameter | Default | Description |
|---|---|---|
CHECK_INTERVAL_SECS | 300 (5 min) | How often to evaluate positions |
MIN_DEPLOY_AMOUNT | 1,000,000 ($1 RWT) | Minimum amount to deploy in one operation |
MAX_POOL_CONCENTRATION | 0.5 (50%) | Max fraction of Nexus capital in one pool |
On-Chain Interaction
| Action | Instruction | Accounts |
|---|---|---|
| Read balance | — | Nexus ATAs (read) |
| Swap | native_dex::nexus_swap | nexus_manager (signer), dex_config, nexus, pool accounts |
| Add LP | native_dex::nexus_add_liquidity | nexus_manager (signer), dex_config, nexus, pool accounts |
| Remove LP | native_dex::nexus_remove_liquidity | nexus_manager (signer), dex_config, nexus, pool accounts |
| Withdraw profits | native_dex::nexus_withdraw_profits | authority (signer), dex_config, nexus (mut), token accounts |
Permissions
- CAN: swap, add/remove liquidity using Nexus PDA’s funds
- CANNOT: withdraw principal, change DEX config, pause pools
- If compromised: attacker can make bad trades with Nexus funds (LP into bad pools, swap at bad prices), but cannot extract principal — protected on-chain (
ata_balance - withdrawal >= principal). Team replaces viaupdate_nexus_manager.
Operational hardening
The Nexus Manager bot uses the shared operational primitives that other privileged Areal bots rely on:- Multi-RPC fallback. Reads and submits go through a multi-endpoint RPC client; security-critical state (the Nexus principal floor, pool reserve depth before a swap) is read with a consensus check across endpoints to defend against single-RPC fault or stale-state attacks.
- Single-instance lock. A PID-file lock at startup prevents two manager instances from racing on the same RPC and the same managed-pool list. A second instance exits immediately with a structured error.
- WS reconcile after disconnect. On a WebSocket reconnect, the bot walks program signatures since the last-seen slot to backfill missed
NexusDepositedandNexusManagerUpdatedevents. - SOL pre-flight check. The bot asserts the manager wallet has enough SOL before the first submit each cycle; if the wallet is dry, the cycle returns a structured
skipwith reasonlow_solrather than failing inside the transaction send call. - Kill-switch authority. When the Nexus Manager is suspected compromised, Authority (Team Multisig) calls
update_nexus_manager(new_manager: [0u8; 32]). The three manager-only DEX instructions (nexus_swap,nexus_add_liquidity,nexus_remove_liquidity) all checksigner != [0u8; 32]and revert withNexusManagerDisabled. Operations stay paused until Authority callsupdate_nexus_manageragain with a real pubkey. - Decision engine. The bot ranks managed pools by drift from a target distribution (default: equal weight across configured pools) and emits one of four decision kinds per cycle:
swap,add_liquidity,remove_liquidity, orskipwith a structured reason. Decisions are persisted append-only on disk in JSONL for after-the-fact review. SEND_TXflag — dry-run mode. The bot defaults toSEND_TX=false(compute and log decisions, but do not submit). Operators flipSEND_TX=truein the production environment after a staging-mode run is verified. Dry-run is the canonical staging-test before live submit.
Shared Infrastructure
All six bots run on a single VPS with shared configuration:Architecture
Wallets
| Bot | Wallet Type | Registered As |
|---|---|---|
| Pool Rebalancer | Dedicated keypair | dex_config.rebalancer |
| Merkle Publisher | Dedicated keypair | config.publish_authority |
| Revenue Crank | Shared crank wallet | Permissionless (no registration) |
| Convert & Fund | Shared crank wallet | Permissionless (no registration) |
| Yield Claim | Shared crank wallet | Permissionless (no registration) |
| Nexus Manager | Dedicated keypair | LiquidityNexus.manager |
Environment Variables
| Variable | Description |
|---|---|
RPC_URL | Solana RPC endpoint (Helius free tier sufficient) |
REBALANCER_KEYPAIR | Pool Rebalancer wallet keypair |
PUBLISHER_KEYPAIR | Merkle Publisher wallet keypair |
NEXUS_MANAGER_KEYPAIR | Nexus Manager wallet keypair |
CRANK_KEYPAIR | Shared wallet for permissionless operations |
RWT_VAULT_ADDRESS | RWT Engine vault PDA |
CONCENTRATED_POOLS | List of concentrated pool addresses (restart required on change) |
OT_PROJECTS | List of {ot_mint, distributor, accumulator, revenue_config} per project (restart required on change) |
Cost Estimate
| Component | Cost/month |
|---|---|
| VPS | $10 |
| Solana TX fees (~500 TXs total) | ~$0.30 |
| RPC (Helius free tier) | $0 |
| Total | ~$10/month |
Operational Requirements
| Requirement | Detail |
|---|---|
| Infrastructure | VPS, always-on, systemd or Docker |
| SOL balance | Each wallet: ~0.1 SOL for TX fees |
| Monitoring | Log every TX with bot name, instruction, result |
| Alerting | Alert if any bot fails for > 15 minutes |
| Key management | Keypairs encrypted at rest, never committed to git |
| Restart policy | Auto-restart on crash, exponential backoff |
Event-Driven Optimizations
Instead of purely interval-based polling, bots can subscribe to Solana events for faster reactions:| Event | Triggers |
|---|---|
CapitalAdjusted (RWT Engine) | Pool Rebalancer checks immediately (NAV changed) |
| Revenue SPL transfer to RevenueAccount | Revenue Crank checks immediately |
StreamConverted (YD) | Merkle Publisher publishes new root (total_funded changed) |
RootPublished (YD) | Yield Claim Crank claims immediately (new proof available) |
Deployment Order
- Deploy all smart contracts
- Bootstrap:
initialize_*all contracts, set authority, register wallets - Register bot wallets on-chain:
update_dex_config(rebalancer: rebalancer_pubkey)— Pool Rebalancerinitialize_nexus(manager: manager_pubkey)— Nexus Manager (first-time singleton creation; subsequent rotation viaupdate_nexus_manager(new_manager); the manager pubkey lives onLiquidityNexus.manager)- Publish authority set at
initialize_config— Merkle Publisher
- Fund bot wallets with ~0.1 SOL each
- Start all services, verify logs
- Transfer authorities to Team Multisig
Trust Summary
| Bot | Trust Level | If Compromised |
|---|---|---|
| Pool Rebalancer | Low | Can shift bins to bad positions. No fund extraction. Replace wallet. |
| Merkle Publisher | High | Can publish fake roots → drain reward vaults. Replace immediately + monitor. |
| Revenue Crank | None | Permissionless. Can only trigger to preset destinations. |
| Convert & Fund | None | Permissionless. Converts to preset vault. |
| Yield Claim | None | Permissionless. Claims to preset destinations. |
| Nexus Manager | Medium | Can make bad trades with Nexus funds. Cannot extract. Replace wallet. |