Skip to main content

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

  1. User calls App: User calls app function (e.g., UpvoteApp.upvote()) with ETH and parameters
  2. App calls Score: App calls Score.execute(strategy, ScoreParams) with {value: msg.value}
  3. Score validates: Score validates scoreDelta != 0 (reverts with ZeroScoreDelta error if zero)
  4. Score updates state: Score updates all state mappings (strategy, app, user, scoreKey dimensions)
  5. Score emits event: Score emits ScoreUpdated event
  6. Score calls Strategy: Score calls strategy.executeScoreStrategy{value: msg.value}() with ScoreStrategyParams
  7. Strategy executes: Strategy performs custom logic (e.g., token swaps), returns metadata
  8. Score stores data: Score stores detailed data in NetStorage (including strategy metadata)
  9. 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 contract
  • netStorage: 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 IScoreStrategy
  • params: ScoreParams struct with scoreKey, scoreDelta, originalSender, contexts

Execution Steps:

  1. Validates params.scoreDelta != 0 (reverts with ZeroScoreDelta)
  2. Increments totalCalls++ (unchecked)
  3. Updates all state mappings via _updateAllScoreState()
  4. Emits ScoreUpdated event
  5. Calls strategy.executeScoreStrategy{value: msg.value}()
  6. Generates storage key: bytes32(totalCalls)
  7. Stores in NetStorage: netStorage.put(storageKey, "", abi.encode(...))
  8. 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 scoreKeys
  • get*User*Scores: Returns scores for specific users
  • getMulti*: Batch queries for multiple items

When to use each:

  • Use get*KeyScores when you know the specific scoreKeys you want to query
  • Use get*TotalScores when 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 scoreKeys
  • getMultiUserStrategyKeyScores: Scores for multiple users and scoreKeys under a strategy
  • getMultiAppUserKeyScores: Scores for multiple apps and scoreKeys for a user
  • getMultiAppStrategyKeyScores: Scores for multiple apps and scoreKeys under a strategy
  • getMultiStrategyUserKeyScores: Scores for multiple strategies and scoreKeys for a user
  • getMultiAppMultiStrategyKeyScores: Scores for multiple apps, strategies, and scoreKeys
  • getMultiUserAppStrategyKeyScores: Scores for multiple users and scoreKeys under an app-strategy
  • getMultiAppStrategyUserKeyScores: Scores for multiple apps and scoreKeys for a user under a strategy
  • getMultiAppMultiUserStrategyKeyScores: 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 metadata stored in NetStorage
  • Unstored Context: Strategies can use scoreUnstoredContext for 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 IScoreStrategy interface
  • Must check msg.sender == CORE_CONTRACT for security
  • Receives msg.value (ETH sent to Score.execute())
  • Can use scoreStoredContext and scoreUnstoredContext for 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 name
  • withdrawETH(address to) external onlyOwner - Withdraw ETH
  • withdrawErc20(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:

  1. User clicks upvote on token page or storage page
  2. User selects count (number of upvotes, 1+)
  3. User pays ETH (0.000025 ETH per upvote)
  4. Website calls UpvoteApp.upvote() with strategy, scoreKey, count, and context
  5. UpvoteApp calls Score.execute() with strategy and ScoreParams
  6. Score updates state and calls strategy.executeScoreStrategy()
  7. Strategy swaps tokens (e.g., ETH → ALPHA, or ETH → Token + ALPHA)
  8. Strategy returns metadata (e.g., token prices, amounts)
  9. Score stores everything in NetStorage and sends Net messages
  10. 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 content
  • storageKey: Storage key (bytes32)
  • scoreDelta: Number of upvotes
  • scoreUnstoredContext: 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 ether
  • feeBps: 250 (2.5%)
  • ALPHA_TOKEN: 0x3D01Fe5A38ddBD307fDd635b4Cb0e29681226D6f
  • WETH: 0x4200000000000000000000000000000000000006
  • SWAP_ROUTER_V3: 0x2626664c2603336E57B271c5C0b26F421741e481

Execution Flow:

  1. Validates msg.sender == CORE_CONTRACT
  2. Rejects negative upvotes
  3. Validates ETH amount: msg.value == scoreDelta * upvotePrice
  4. Calculates fee: fee = msgValue * feeBps / 10000
  5. Calculates ETH to swap: ethToSwap = msgValue - fee
  6. Swaps ethToSwap for ALPHA, sends to originalSender
  7. If fee > 0, swaps fee for ALPHA, sends 50% to app if app is contract
  8. 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 ether
  • feeBps: 250 (2.5%)
  • Same token addresses as PureAlpha

Execution Flow:

  1. Validates msg.sender == CORE_CONTRACT
  2. Rejects negative upvotes
  3. Validates ETH amount: msg.value == scoreDelta * upvotePrice
  4. Extracts token address from scoreKey
  5. Decodes PoolKey from scoreStoredContext
  6. Splits ETH: half for token swap, half for ALPHA swap, fees split
  7. Swaps half ETH for target token (V2/V3/V4 based on PoolKey)
  8. Swaps other half ETH for ALPHA
  9. Swaps fee halves for token and ALPHA
  10. Sends 50% of fees to app if app is contract
  11. Sends token-gated chat message
  12. 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 ether
  • feeBps: 250 (2.5%)
  • tokenSplitBps: Configurable split between token and ALPHA
  • Same token addresses as PureAlpha

Execution Flow:

  1. Validates msg.sender == CORE_CONTRACT
  2. Rejects negative upvotes
  3. Validates ETH amount: msg.value == scoreDelta * upvotePrice
  4. Extracts token address from scoreKey
  5. Decodes PoolKey from scoreStoredContext
  6. Calculates fee: fee = msgValue * feeBps / 10000
  7. Calculates ETH to swap: ethToSwap = msgValue - fee
  8. Calculates dynamic split: tokenAmountETH = ethToSwap * tokenSplitBps / 10000, alphaAmountETH = ethToSwap - tokenAmountETH
  9. Swaps tokenAmountETH for target token (V2/V3/V4 based on PoolKey)
  10. Swaps alphaAmountETH for ALPHA
  11. Swaps fee halves for token and ALPHA
  12. Sends 50% of fees to app if app is contract
  13. Sends token-gated chat message
  14. 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)

  1. Query Net messages by topic to find storage keys
  2. Read NetStorage using storage keys to get detailed score data
  3. 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:

ContractAddress
Score0x0000000FA09B022E5616E5a173b4b67FA2FBcF28
UpvoteApp0x00000001f0b8173316a016a5067ad74e8cea47bf
UpvoteStorageApp0x000000060CEB69D023227DF64CfB75eC37c75B62
UpvotePureAlphaStrategy0x00000001b1bcdeddeafd5296aaf4f3f3e21ae876
UpvoteUniv234PoolsStrategy0x000000063f84e07a3e7a7ee578b42704ee6d22c9
UpvoteDynamicSplitUniv234PoolsStrategy0x0000000869160f0b2a213adefb46a7ea7e62ac7a

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.