Score Protocol Developer Guide
Learn how to integrate with Score Protocol in your applications. This guide covers smart contract interactions, querying patterns, and best practices for building on Score Protocol.
How Score Protocol Works
Core Architecture
Score Protocol uses a three-layer architecture:
- Score Contract: Core contract that manages all scoring state and coordinates execution
- Apps: Contracts that call Score to update scores (e.g., UpvoteApp, UpvoteStorageApp)
- Strategies: Contracts that execute custom logic when scores change (e.g., token swaps, economic mechanisms)
Key Concept: Score uses a strategy-based approach where apps call Score with a strategy contract. The strategy executes custom logic (e.g., token swaps, calculations, economic mechanisms) when scores change. This enables extensibility - any app can use any strategy, and developers can create new strategies with custom logic without modifying existing apps.
Execution Flow
Complete Flow: User → App → Score → Strategy
- User calls App: User calls app function (e.g.,
UpvoteApp.upvote()) with ETH and parameters - App calls Score: App calls
Score.execute(strategy, ScoreParams)with{value: msg.value} - Score validates: Score validates
scoreDelta != 0(reverts withZeroScoreDeltaerror if zero) - Score updates state: Score updates all state mappings (strategy, app, user, scoreKey dimensions)
- Score emits event: Score emits
ScoreUpdatedevent - Score calls Strategy: Score calls
strategy.executeScoreStrategy{value: msg.value}()with ScoreStrategyParams - Strategy executes: Strategy performs custom logic (e.g., token swaps), returns metadata
- Score stores data: Score stores detailed data in NetStorage (including strategy metadata)
- Score sends Net messages: Score sends 6 Net messages for indexing and querying
Transaction Revert Behavior: If any contract (App, Score, or Strategy) reverts, the entire transaction reverts. For example, upvote strategies revert if scoreDelta is negative because only positive upvotes are supported.
Key Point: Apps are the entry point for users. Apps call Score.execute() with a strategy. Score coordinates everything and calls the strategy. Strategies execute custom logic and return metadata.
ScoreParams Structure
Every score update uses this structure:
struct ScoreParams {
bytes32 scoreKey; // What's being scored (token address, storage key hash, etc.)
int256 scoreDelta; // Score change (can be positive or negative)
address originalSender; // User who triggered the score change
bytes scoreStoredContext; // Context data stored in NetStorage (e.g., PoolKey for tokens)
bytes scoreUnstoredContext; // Context data not stored (can be arbitrary outside data)
}
Context Fields:
scoreStoredContext: Data stored in NetStorage (e.g., Uniswap PoolKey for tokens). This data is permanently stored and can be queried later.scoreUnstoredContext: Arbitrary outside data that powers strategy (e.g., multi-hop 0x swap route). Not stored onchain, only passed to strategy during execution.
Core Contract (Score.sol)
Contract Information
Contract Name: Score
Contract Address: 0x0000000FA09B022E5616E5a173b4b67FA2FBcF28
Deployment: Base only (currently)
Core Constants
NET:0x00000000B24D62781dB359b07880a105cD0b64e6- Net Protocol contractnetStorage:0x00000000DB40fcB9f4466330982372e27Fd7Bbf5- NetStorage contract
State Variables
Score maintains multi-dimensional state mappings internally for efficient querying. Developers interact with scores through query functions rather than accessing state mappings directly.
Execute Function
function execute(
IScoreStrategy strategy,
ScoreParams calldata params
) external payable nonReentrant
Parameters:
strategy: Strategy contract address implementing IScoreStrategyparams: ScoreParams struct with scoreKey, scoreDelta, originalSender, contexts
Execution Steps:
- Validates
params.scoreDelta != 0(reverts with ZeroScoreDelta) - Increments
totalCalls++(unchecked) - Updates all state mappings via
_updateAllScoreState() - Emits
ScoreUpdatedevent - Calls
strategy.executeScoreStrategy{value: msg.value}() - Generates storage key:
bytes32(totalCalls) - Stores in NetStorage:
netStorage.put(storageKey, "", abi.encode(...)) - Sends Net messages:
_sendAppNetMessages()
Events:
event ScoreUpdated(
address indexed user,
address indexed app,
address indexed strategy,
int256 scoreDelta,
bytes32 scoreKey
);
Errors:
error ZeroScoreDelta();
Net Message Topics
Score sends 6 Net messages with different topic prefixes for indexing:
"s" + strategy + scoreKey: Strategy key scores message"t" + strategy: Strategy total scores message"a" + app: App total scores message"k" + app + scoreKey: App key scores message"u" + app + strategy + scoreKey: App strategy key scores message"v" + app + strategy: App strategy total scores message
Each message contains abi.encode(storageKey) as data, allowing queries to find the NetStorage key for detailed score data.
Indexing Approach: Each score is indexed and tracked based on combination of app, strategy, original sender, and score key. This allows fetching complete history of score changes and relevant metadata in a fully onchain way. Projects can query only data from specific apps, strategies, score keys, and/or users they want.
Query Functions
All query functions are view functions that return int256 (scores can be negative). All use unchecked blocks for gas optimization.
Understanding scoreKey
A scoreKey is a bytes32 identifier for what's being scored. Examples:
- Token address padded to bytes32:
0x0000000000000000000000003d01fe5a38ddbd307fdd635b4cb0e29681226d6f - Hash of storage content:
keccak256(abi.encodePacked(storageKey, operatorAddress)) - Any custom bytes32 identifier your app defines
Scores are tracked per scoreKey, allowing you to query scores for specific items.
Query Pattern
Score provides query functions across multiple dimensions. Functions follow naming patterns:
get*TotalScores: Returns total scores (sum across all scoreKeys)get*KeyScores: Returns scores for specific scoreKeysget*User*Scores: Returns scores for specific usersgetMulti*: Batch queries for multiple items
When to use each:
- Use
get*KeyScoreswhen you know the specific scoreKeys you want to query - Use
get*TotalScoreswhen you want aggregate scores across all items - Use
getMulti*functions when querying multiple items to reduce RPC calls
Key Query Functions
By Strategy:
function getStrategyTotalScores(
address[] calldata strategies
) external view returns (int256[] memory totals)
Returns total scores for each strategy.
function getStrategyKeyScores(
address strategy,
bytes32[] calldata scoreKeys
) external view returns (int256[] memory counts)
Returns scores for specific scoreKeys under a strategy.
By App:
function getAppTotalScores(
address[] calldata apps
) external view returns (int256[] memory totals)
Returns total scores for each app.
function getAppKeyScores(
address app,
bytes32[] calldata scoreKeys
) external view returns (int256[] memory counts)
Returns scores for specific scoreKeys under an app.
By User:
function getAppUserKeyScores(
address app,
address user,
bytes32[] calldata scoreKeys
) external view returns (int256[] memory counts)
Returns user's scores for specific scoreKeys under an app.
Combined Dimensions:
function getAppStrategyKeyScores(
address app,
address strategy,
bytes32[] calldata scoreKeys
) external view returns (int256[] memory counts)
Returns scores for specific scoreKeys under an app-strategy combination.
function getAppStrategyUserKeyScores(
address app,
address strategy,
address user,
bytes32[] calldata scoreKeys
) external view returns (int256[] memory counts)
Returns user's scores for specific scoreKeys under an app-strategy combination.
Batch Query Functions
Score provides batch query functions for efficient multi-item queries. Use these when you need to query multiple items at once to reduce RPC calls:
getMultiStrategyKeyScores: Sum scores across multiple strategies for the same scoreKeysgetMultiUserStrategyKeyScores: Scores for multiple users and scoreKeys under a strategygetMultiAppUserKeyScores: Scores for multiple apps and scoreKeys for a usergetMultiAppStrategyKeyScores: Scores for multiple apps and scoreKeys under a strategygetMultiStrategyUserKeyScores: Scores for multiple strategies and scoreKeys for a usergetMultiAppMultiStrategyKeyScores: Scores for multiple apps, strategies, and scoreKeysgetMultiUserAppStrategyKeyScores: Scores for multiple users and scoreKeys under an app-strategygetMultiAppStrategyUserKeyScores: Scores for multiple apps and scoreKeys for a user under a strategygetMultiAppMultiUserStrategyKeyScores: Scores for multiple apps, users, and scoreKeys under a strategy
Example use case: Displaying upvote counts for multiple tokens on a page. Instead of calling getStrategyKeyScores once per token, use getMultiStrategyKeyScores with an array of scoreKeys.
Strategy Interface
IScoreStrategy Interface
interface IScoreStrategy {
struct ScoreStrategyParams {
bytes32 scoreKey;
int256 scoreDelta;
address originalSender;
address appAddress;
bytes scoreStoredContext;
bytes scoreUnstoredContext;
}
function executeScoreStrategy(
ScoreStrategyParams calldata params
) external payable returns (bytes memory metadata);
}
How Strategies Work
- Called by Score: Strategies must check
msg.sender == CORE_CONTRACT - Receives ETH: Strategies receive
msg.value(ETH sent to Score.execute()) - Custom Logic: Strategies can execute any logic (token swaps, calculations, etc.)
- Returns Metadata: Strategies return
bytes memory metadatastored in NetStorage - Unstored Context: Strategies can use
scoreUnstoredContextfor arbitrary outside data (e.g., 0x swap routes)
Extensibility: Developers can create new strategies for different DEXs (Aerodrome, Zora, Virtuals, etc.) or custom logic. Anyone can pass a new strategy to existing apps like UpvoteApp.
Example Strategy
import {IScoreStrategy} from "./IScoreStrategy.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MyCustomStrategy is IScoreStrategy, Ownable {
address internal constant CORE_CONTRACT =
address(0x0000000FA09B022E5616E5a173b4b67FA2FBcF28);
function executeScoreStrategy(
IScoreStrategy.ScoreStrategyParams calldata params
) external payable returns (bytes memory metadata) {
// Must check caller is Score contract
if (msg.sender != CORE_CONTRACT) revert Unauthorized();
// Execute custom logic (e.g., swap tokens, calculations, etc.)
// Can use params.scoreStoredContext and params.scoreUnstoredContext
// Return metadata to be stored in NetStorage
return abi.encode(/* your metadata */);
}
}
Key Requirements:
- Must implement
IScoreStrategyinterface - Must check
msg.sender == CORE_CONTRACTfor security - Receives
msg.value(ETH sent to Score.execute()) - Can use
scoreStoredContextandscoreUnstoredContextfor custom logic - Must return
bytes memory metadata(stored in NetStorage)
Building Score Apps
ScoreBasicApp
Abstract base class for Score apps.
Constructor:
constructor(address _coreContract, address _owner) Ownable(_owner)
Functions:
name() external view virtual returns (string memory)- App namewithdrawETH(address to) external onlyOwner- Withdraw ETHwithdrawErc20(address to, address token, uint256 amount) external onlyOwner- Withdraw ERC20
CORE_CONTRACT: Immutable reference to Score contract.
Example App
import {ScoreBasicApp} from "./ScoreBasicApp.sol";
import {Score} from "./Score.sol";
import {IScoreStrategy} from "./IScoreStrategy.sol";
contract MyScoreApp is ScoreBasicApp {
constructor() ScoreBasicApp(
address(0x0000000FA09B022E5616E5a173b4b67FA2FBcF28), // Score
address(0x...) // owner
) {}
function name() external pure override returns (string memory) {
return "MyScoreApp";
}
function updateScore(
IScoreStrategy strategy,
bytes32 scoreKey,
uint256 scoreDelta,
bytes calldata context
) external payable {
CORE_CONTRACT.execute{value: msg.value}(
strategy,
Score.ScoreParams({
scoreKey: scoreKey,
scoreDelta: int256(scoreDelta),
originalSender: msg.sender,
scoreStoredContext: context,
scoreUnstoredContext: ""
})
);
}
}
How Apps and Strategies Work Together:
- Apps are entry points: Users interact with apps, not Score directly
- Apps choose strategies: Apps can choose strategies themselves or allow users to pass in a strategy as a parameter, which allows the same app to work with multiple strategies
- Apps call Score: Apps call
Score.execute()with the chosen strategy - Score calls strategy: Score then calls the strategy's
executeScoreStrategy()function - Mix and match: Any app can use any strategy. For example, UpvoteApp accepts a strategy parameter, allowing it to work with PureAlpha, Univ234Pools, or any new custom strategy
Custom Apps: Developers can create entirely independent apps that use Score, take fees, add custom logic, etc. Apps and strategies can be mixed and matched.
Upvoting
Upvoting is the first use case for Score Protocol. Users upvote tokens or storage content, and Score Protocol tracks the scores.
How Upvoting Works
Complete Flow:
- User clicks upvote on token page or storage page
- User selects count (number of upvotes, 1+)
- User pays ETH (0.000025 ETH per upvote)
- Website calls UpvoteApp.upvote() with strategy, scoreKey, count, and context
- UpvoteApp calls Score.execute() with strategy and ScoreParams
- Score updates state and calls strategy.executeScoreStrategy()
- Strategy swaps tokens (e.g., ETH → ALPHA, or ETH → Token + ALPHA)
- Strategy returns metadata (e.g., token prices, amounts)
- Score stores everything in NetStorage and sends Net messages
- Website queries scores to display updated upvote count
User Experience
- Users see upvote badges/buttons on token pages and storage pages
- Clicking opens a dialog to select number of upvotes (1+)
- Cost: 0.000025 ETH per upvote (fixed price)
- Users can see current upvote count
- After upvoting, count updates immediately
UpvoteApp
Contract Address: 0x00000001f0b8173316a016a5067ad74e8cea47bf
Function:
function upvote(
IScoreStrategy strategy,
bytes32 scoreKey,
uint256 scoreDelta,
bytes calldata scoreStoredContext,
bytes calldata scoreUnstoredContext
) external payable
Parameters:
strategy: Strategy contract (PureAlpha or Univ234Pools)scoreKey: Token address as bytes32 (padded)scoreDelta: Number of upvotes (1+)scoreStoredContext: Encoded PoolKey for token (for Univ234Pools strategy)scoreUnstoredContext: Empty ("0x")
Legacy Support:
function getUpvotesWithLegacy(
bytes32[] calldata upvoteKeys,
address[] calldata strategies
) external view returns (uint256[] memory counts)
Returns total upvotes including legacy system compatibility.
UpvoteStorageApp
Contract Address: 0x000000060CEB69D023227DF64CfB75eC37c75B62
Function:
function upvote(
IScoreStrategy strategy,
address storageOperatorAddress,
bytes32 storageKey,
uint256 scoreDelta,
bytes calldata scoreUnstoredContext
) external payable
Parameters:
strategy: Strategy contract (always PureAlpha for storage)storageOperatorAddress: Address that stored the contentstorageKey: Storage key (bytes32)scoreDelta: Number of upvotesscoreUnstoredContext: Empty ("0x")
Implementation Details:
- Measures ALPHA balance before execution
- Constructs
scoreStoredContext:abi.encode(storageKey, storageOperatorAddress) - Generates
scoreKey:keccak256(abi.encodePacked(storageKey, storageOperatorAddress)) - Forwards ALPHA received to storage operator after execution
Score Key Generation
Token Score Keys:
- Token address padded to bytes32 (starts with "0x000000000000000000000000")
- Last 20 bytes are the token address
- Example:
0x0000000000000000000000003d01fe5a38ddbd307fdd635b4cb0e29681226d6f
Storage Score Keys:
- Hash of storage key + operator address
keccak256(abi.encodePacked(storageKey, operatorAddress))- Different from token keys (not padded address)
Strategy Selection
- PureAlpha Strategy: Used when token has no Uniswap pool or pool is invalid
- Univ234Pools Strategy: Used when token has valid Uniswap V2/V3/V4 pool
- Dynamic Split Strategy: Used when token has valid Uniswap V2/V3/V4 pool and configurable token/alpha split is preferred
- Storage: Always uses PureAlpha strategy
UpvotePureAlphaStrategy
Contract Address: 0x00000001b1bcdeddeafd5296aaf4f3f3e21ae876
Function:
function executeScoreStrategy(
IScoreStrategy.ScoreStrategyParams calldata params
) external payable nonReentrant returns (bytes memory metadata)
Configuration:
upvotePrice: 0.000025 etherfeeBps: 250 (2.5%)ALPHA_TOKEN:0x3D01Fe5A38ddBD307fDd635b4Cb0e29681226D6fWETH:0x4200000000000000000000000000000000000006SWAP_ROUTER_V3:0x2626664c2603336E57B271c5C0b26F421741e481
Execution Flow:
- Validates
msg.sender == CORE_CONTRACT - Rejects negative upvotes
- Validates ETH amount:
msg.value == scoreDelta * upvotePrice - Calculates fee:
fee = msgValue * feeBps / 10000 - Calculates ETH to swap:
ethToSwap = msgValue - fee - Swaps
ethToSwapfor ALPHA, sends tooriginalSender - If fee > 0, swaps fee for ALPHA, sends 50% to app if app is contract
- Returns metadata:
abi.encode(alphaAmount, alphaWethPrice, wethUsdcPrice, userAlphaBalance)
What it does: Swaps all upvote funds to ALPHA tokens and gives them to the original sender (less fees).
UpvoteUniv234PoolsStrategy
Contract Address: 0x000000063f84e07a3e7a7ee578b42704ee6d22c9
Function:
function executeScoreStrategy(
IScoreStrategy.ScoreStrategyParams calldata params
) external payable nonReentrant returns (bytes memory metadata)
Configuration:
upvotePrice: 0.000025 etherfeeBps: 250 (2.5%)- Same token addresses as PureAlpha
Execution Flow:
- Validates
msg.sender == CORE_CONTRACT - Rejects negative upvotes
- Validates ETH amount:
msg.value == scoreDelta * upvotePrice - Extracts token address from
scoreKey - Decodes
PoolKeyfromscoreStoredContext - Splits ETH: half for token swap, half for ALPHA swap, fees split
- Swaps half ETH for target token (V2/V3/V4 based on PoolKey)
- Swaps other half ETH for ALPHA
- Swaps fee halves for token and ALPHA
- Sends 50% of fees to app if app is contract
- Sends token-gated chat message
- Returns metadata:
abi.encode(tokenAmount, tokenWethPrice, wethUsdcPrice, alphaWethPrice, userTokenBalance)
Protocol Detection:
- V4:
tickSpacing != 0 - V3:
fee != 0 && hooks == address(0) - V2:
fee == 0 && hooks == address(0)
What it does: Takes a Uniswap pool key for a token as stored context and swaps 50% of upvote funds to the token and 50% to ALPHA (less fees), giving them to the original sender. Returns metadata representing price of token at time of swap.
UpvoteDynamicSplitUniv234PoolsStrategy
Contract Address: 0x0000000869160f0b2a213adefb46a7ea7e62ac7a
Function:
function executeScoreStrategy(
IScoreStrategy.ScoreStrategyParams calldata params
) external payable nonReentrant returns (bytes memory metadata)
Configuration:
upvotePrice: 0.000025 etherfeeBps: 250 (2.5%)tokenSplitBps: Configurable split between token and ALPHA- Same token addresses as PureAlpha
Execution Flow:
- Validates
msg.sender == CORE_CONTRACT - Rejects negative upvotes
- Validates ETH amount:
msg.value == scoreDelta * upvotePrice - Extracts token address from
scoreKey - Decodes
PoolKeyfromscoreStoredContext - Calculates fee:
fee = msgValue * feeBps / 10000 - Calculates ETH to swap:
ethToSwap = msgValue - fee - Calculates dynamic split:
tokenAmountETH = ethToSwap * tokenSplitBps / 10000,alphaAmountETH = ethToSwap - tokenAmountETH - Swaps
tokenAmountETHfor target token (V2/V3/V4 based on PoolKey) - Swaps
alphaAmountETHfor ALPHA - Swaps fee halves for token and ALPHA
- Sends 50% of fees to app if app is contract
- Sends token-gated chat message
- Returns metadata:
abi.encode(tokenAmount, tokenWethPrice, wethUsdcPrice, alphaWethPrice, userTokenBalance)
Protocol Detection:
- V4:
tickSpacing != 0 - V3:
fee != 0 && hooks == address(0) - V2:
fee == 0 && hooks == address(0)
What it does: Takes a Uniswap pool key for a token as stored context and swaps a configurable portion of upvote funds to the token and the remainder to ALPHA (less fees), giving them to the original sender. The split ratio is configurable via tokenSplitBps. Returns metadata representing price of token at time of swap.
Net Integration
NetStorage Usage
Score stores detailed data in NetStorage using key bytes32(totalCalls).
Stored Data:
abi.encode(
scoreKey,
scoreDelta,
originalSender,
app, // msg.sender (app address)
strategy,
block.timestamp,
scoreStoredContext,
scoreUnstoredContext,
metadata // Returned by strategy
)
This allows querying historical score data.
Net Protocol Messages
Score sends 6 Net messages with different topic prefixes:
- Strategy key scores:
"s" + strategy + scoreKey - Strategy total scores:
"t" + strategy - App total scores:
"a" + app - App key scores:
"k" + app + scoreKey - App strategy key scores:
"u" + app + strategy + scoreKey - App strategy total scores:
"v" + app + strategy
Each message contains abi.encode(storageKey) as data.
Querying Score Data from Net
Score data can be queried in two ways:
Option 1: Direct Contract Queries (Recommended for simple queries)
- Use Score contract query functions directly
- Fast and efficient for known parameters
- Best for: Getting current scores, displaying upvote counts
Option 2: Net Protocol Queries (For complex queries and historical data)
- Query Net messages by topic to find storage keys
- Read NetStorage using storage keys to get detailed score data
- Enables building score feeds, leaderboards, historical analysis
How upvotes are fetched: The Net website uses Net Protocol queries to fetch and display upvotes - by querying Net messages and NetStorage. This allows building complex queries across apps, strategies, and users.
Contract Addresses
All contracts deployed on Base:
| Contract | Address |
|---|---|
| Score | 0x0000000FA09B022E5616E5a173b4b67FA2FBcF28 |
| UpvoteApp | 0x00000001f0b8173316a016a5067ad74e8cea47bf |
| UpvoteStorageApp | 0x000000060CEB69D023227DF64CfB75eC37c75B62 |
| UpvotePureAlphaStrategy | 0x00000001b1bcdeddeafd5296aaf4f3f3e21ae876 |
| UpvoteUniv234PoolsStrategy | 0x000000063f84e07a3e7a7ee578b42704ee6d22c9 |
| UpvoteDynamicSplitUniv234PoolsStrategy | 0x0000000869160f0b2a213adefb46a7ea7e62ac7a |
Note: Score is a free onchain public good powered by Net Protocol on Base. The code is not audited and unexpected issues may occur. Use at your own risk. Not financial advice.