Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/hypertekorg/hyperstack/llms.txt

Use this file to discover all available pages before exploring further.

Population strategies define how an entity’s state is updated when new data arrives from the blockchain. Choosing the right strategy is critical for ensuring your projected state accurately reflects the underlying on-chain activity.

What are Population Strategies?

When a Hyperstack handler processes a transaction or account update, it maps data from the source (e.g., an Anchor instruction or account field) into your entity’s fields. A Population Strategy determines how that incoming value interacts with the existing value in that field. For example, should a “Total Volume” field be overwritten by the latest trade amount, or should the latest amount be added to the current total? Strategies answer this question.

Strategy Selection Guide

Use this decision tree to identify the correct strategy for your field:

Quick Reference Table

StrategyBehaviorBest For
LastWriteOverwrites with newest data (Default)Balances, Status, Current Prices
SetOnceOnly sets if field is emptyIDs, Creation Timestamps, Owners
SumAdds incoming value to currentVolume, Total Rewards, TVL
CountIncrements by 1Trade Count, User Count, Event Count
AppendAdds to an array/listTrade History, Activity Logs
Max / MinTracks the peak/troughHigh/Low Prices, Peak Liquidity
UniqueCountCounts distinct occurrencesActive Users, Unique Voters
MergeMerges keys in an objectConfiguration, Metadata

Detailed Reference

LastWrite (Default)

The most common strategy. Whenever a new value arrives, it completely replaces the previous one.
  • Use Case: Tracking the current state of an account
  • Example:
    #[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
    pub total_deployed: Option<u64>,
    
Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundState {
    #[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite)]
    pub motherlode: Option<u64>,

    #[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
    pub total_deployed: Option<u64>,

    #[map(ore_sdk::accounts::Round::total_miners, strategy = LastWrite)]
    pub total_miners: Option<u64>,
}

SetOnce

The field is populated only once. Subsequent updates for the same entity will ignore this mapping if the field already has a value.
  • Use Case: Immutable properties or “First Seen” metadata
  • Example:
    #[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
    pub round_id: u64,
    
Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundId {
    #[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
    pub round_id: u64,

    #[map(ore_sdk::accounts::Round::__account_address, lookup_index, strategy = SetOnce)]
    pub round_address: String,
}

Sum

Numeric values are added to the existing value. This is the foundation of tracking volume and throughput.
  • Use Case: Financial metrics and cumulative totals
  • Example:
    #[aggregate(from = ore_sdk::instructions::Deploy, field = args::amount, strategy = Sum, lookup_by = accounts::round)]
    pub total_deployed_amount: Option<u64>,
    
How it works:
  1. Initial state: total_deployed_amount = 0
  2. Deploy with amount = 1000total_deployed_amount = 1000
  3. Deploy with amount = 500total_deployed_amount = 1500
  4. Deploy with amount = 2000total_deployed_amount = 3500

Count

Ignores the incoming value and simply increments the field by 1 for every match.
  • Use Case: Tracking throughput or frequency
  • Example:
    #[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
    pub deploy_count: Option<u64>,
    
Real example from ORE stack:
#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundMetrics {
    // Count of deploy instructions for this round
    #[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
    pub deploy_count: Option<u64>,

    // Count of checkpoint instructions for this round
    #[aggregate(from = ore_sdk::instructions::Checkpoint, strategy = Count, lookup_by = accounts::round)]
    pub checkpoint_count: Option<u64>,
}

Append

Adds the incoming value to a list. Use this to maintain a linear history of events within an entity.
  • Use Case: Event logs, audit trails
  • Example:
    #[event(from = Liquidate, strategy = Append)]
    pub liquidation_history: Vec<LiquidationEvent>,
    
Warning: Append grows the size of your entity state. Avoid appending thousands of items to a single entity if you only need the latest state.

Max / Min

Keeps the highest or lowest value seen across all updates.
  • Use Case: 24h Highs/Lows, price discovery
  • Example:
    #[aggregate(from = OracleUpdate, field = price, strategy = Max)]
    pub all_time_high: u64,
    

UniqueCount

Maintains a set of unique values internally but projects only the count of that set.
  • Use Case: Tracking “Active Users” or unique participants
  • Example:
    #[aggregate(from = Vote, field = accounts::voter, strategy = UniqueCount)]
    pub total_unique_voters: u32,
    

Merge

Used for object-like fields where you want to update specific keys without overwriting the entire object.
  • Use Case: Dynamic configuration maps
  • Example:
    #[map(from = Config, strategy = Merge)]
    pub metadata: Map<String, String>,
    

Common Patterns

Token Price Tracking

FieldStrategyWhy
current_priceLastWriteYou only care about the most recent oracle update
day_highMaxTracks the peak price seen in the stream
daily_volumeSumEvery swap adds to the total

Governance Tracking

FieldStrategyWhy
total_votesSumSum of vote weights
voter_countUniqueCountCount unique public keys that voted
first_vote_atSetOnceRecord when the first vote was cast

Mining Round Tracking (ORE)

FieldStrategyWhy
round_idSetOnceID never changes
total_deployedLastWriteAccount stores current total
deploy_countCountCount each Deploy instruction
motherlodeLastWritePrize pool updated by program

Real Example: Complete Entity

From the ORE stack, combining multiple strategies:
#[entity(name = "OreRound")]
#[view(name = "latest", sort_by = "id.round_id", order = "desc")]
pub struct OreRound {
    pub id: RoundId,
    pub state: RoundState,
    pub metrics: RoundMetrics,
}

#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundId {
    // SetOnce: ID never changes
    #[map(ore_sdk::accounts::Round::id, primary_key, strategy = SetOnce)]
    pub round_id: u64,

    // SetOnce: Address never changes
    #[map(ore_sdk::accounts::Round::__account_address, lookup_index, strategy = SetOnce)]
    pub round_address: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundState {
    // LastWrite: Track current value
    #[map(ore_sdk::accounts::Round::motherlode, strategy = LastWrite)]
    pub motherlode: Option<u64>,

    // LastWrite: Track current value
    #[map(ore_sdk::accounts::Round::total_deployed, strategy = LastWrite)]
    pub total_deployed: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Stream)]
pub struct RoundMetrics {
    // Count: Increment for each Deploy instruction
    #[aggregate(from = ore_sdk::instructions::Deploy, strategy = Count, lookup_by = accounts::round)]
    pub deploy_count: Option<u64>,

    // Count: Increment for each Checkpoint instruction
    #[aggregate(from = ore_sdk::instructions::Checkpoint, strategy = Count, lookup_by = accounts::round)]
    pub checkpoint_count: Option<u64>,
}

Common Mistakes to Avoid

1. Using LastWrite for Volume

If you use LastWrite for a volume field, it will only show the amount of the most recent trade, not the total. Wrong:
#[map(Trade::amount, strategy = LastWrite)]  // Only shows last trade!
pub total_volume: u64,
Correct:
#[aggregate(from = Trade, field = args::amount, strategy = Sum)]
pub total_volume: u64,

2. Forgetting SetOnce for IDs

If you use LastWrite for an ID field, it might be overwritten unexpectedly. Wrong:
#[map(Round::id, primary_key, strategy = LastWrite)]  // ID could change!
pub round_id: u64,
Correct:
#[map(Round::id, primary_key, strategy = SetOnce)]
pub round_id: u64,

3. Appending Unnecessarily

Append grows entity state linearly. For high-frequency events, this can become a problem. Wrong:
#[event(from = Trade, fields = [amount, price], strategy = Append)]  // Could be 1000s of trades!
pub all_trades: Vec<Trade>,
Better:
// Track aggregates instead
#[aggregate(from = Trade, strategy = Count)]
pub trade_count: u64,

#[aggregate(from = Trade, field = args::amount, strategy = Sum)]
pub total_volume: u64,

4. Not Specifying Strategy for Aggregates

Always explicitly state the strategy for aggregations to ensure clarity. Wrong:
#[aggregate(from = Deploy)]  // What should this do?
pub deploys: ???,
Correct:
#[aggregate(from = Deploy, strategy = Count)]
pub deploy_count: u64,

Next Steps

Build docs developers (and LLMs) love