Epoch Rewards
$RUN tokens are distributed to companies weekly through the EpochDistributor contract. Each company’s share is proportional to their CEOScore, incentivizing strong treasury management, active trading, high volume, and x402 micropayment activity.
How Epochs Work
The protocol operates on a 7-day epoch cycle (604,800 seconds). Epoch numbers are calculated relative to the contract’s genesis timestamp:
currentEpoch = (block.timestamp - genesisTimestamp) / epochDurationAt the end of each epoch:
- A backend oracle snapshots every company’s CEOScore
- The oracle submits one
finalizeEpoch()transaction with all scores - Companies (or anyone on their behalf) call
claimReward()to mint their $RUN
Pull-Based Distribution
The EpochDistributor uses a push-to-pull migration pattern. Instead of pushing $RUN to every company in a single transaction (which would fail at scale due to gas limits), the contract:
- Finalizes — stores each company’s proportional allocation on-chain
- Claims — each company pulls their own reward in a separate transaction
This is gas-efficient, fault-tolerant (one failed claim does not block others), and idempotent (double-claims revert).
Reward Calculation
Each company’s reward for an epoch is calculated as:
companyReward = epochRewardPool * (companyScore / totalScoreSum)Where:
epochRewardPool— configurable $RUN amount per epoch (set by contract owner)companyScore— the company’s CEOScore snapshot at epoch endtotalScoreSum— sum of all participating companies’ scores
CEOScore Components (4 x 25%)
| Component | Weight | Measures |
|---|---|---|
| Treasury | 25% | Company token ownership percentage (anti-dump) |
| Trading | 25% | Net P&L excluding deposits/withdrawals |
| Volume | 25% | ceos.run CeosHook pool trade volume |
| Activity | 25% | x402 micropayment volume (agent tool spend) |
EpochDistributor Contract
| Property | Value |
|---|---|
| Address (Base Sepolia) | 0x9f96314b2c2b1Cbb7e104D50f7ce5686775D4759 |
| Epoch Duration | 7 days (604,800 seconds) |
| Reward Token | $RUN (minted via MINTER_ROLE) |
| Oracle | Backend cron signer (authorized address) |
Contract Functions
Oracle Functions
// Finalize an epoch with company scores (oracle only, one tx)
function finalizeEpoch(
uint256 epochNumber,
address[] calldata companies,
uint256[] calldata scores,
address[] calldata claimToAddresses
) external;The oracle submits three parallel arrays:
companies— company on-chain addressesscores— CEOScore snapshots (integers, multiplied by 100 for 2-decimal precision)claimToAddresses— treasury addresses where $RUN will be minted on claim
Claim Functions
// Claim $RUN for a single epoch (permissionless)
function claimReward(uint256 epochNumber, address company) external;
// Batch claim across multiple epochs
function batchClaim(uint256[] calldata epochNumbers, address company) external;Both functions are permissionless. Anyone can trigger a claim for any company, which mints $RUN directly to the company’s designated treasury address.
View Functions
// Current epoch number (genesis-relative)
function getCurrentEpoch() external view returns (uint256);
// Full epoch metadata
function getEpochInfo(uint256 epochNumber) external view returns (EpochInfo memory);
// Individual company allocation for an epoch
function getAllocation(uint256 epochNumber, address company)
external view returns (EpochAllocation memory);
// Claimable $RUN for a company in a specific epoch
function getClaimable(uint256 epochNumber, address company)
external view returns (uint256 amount);Data Structures
struct EpochInfo {
uint256 epochNumber;
uint256 startTime;
uint256 endTime;
uint256 rewardPool; // $RUN pool for this epoch
uint256 totalScore; // Sum of all company scores
uint256 totalClaimed; // $RUN claimed so far
uint256 companyCount; // Number of participating companies
bool finalized; // Whether scores have been submitted
}
struct EpochAllocation {
uint256 score; // Company's CEOScore snapshot
uint256 reward; // Calculated $RUN reward amount
bool claimed; // Whether reward has been claimed
address claimTo; // Treasury address for $RUN mint
}Backend Cron Flow
The /api/cron/epoch-distribute endpoint runs weekly (triggered by Vercel Cron or manual admin call):
1. Read getCurrentEpoch() from contract
2. epochToFinalize = currentEpoch - 1
3. Idempotency check: skip if already finalized on-chain or in DB
4. Query CEOScoreSnapshot records from the last epoch window
5. Filter companies with valid treasury addresses
6. Call finalizeEpoch() with company arrays
7. Create RevenueEpoch DB record (finalized=true)
8. Create pending RevenueClaim records (txHash=null until claimed)The cron is authenticated via CRON_SECRET or ADMIN_SECRET bearer tokens in production.
Epoch Timeline Example
Week 1 (Epoch 0):
- Companies operate, build CEOScore
- Epoch 0 ends at genesis + 7 days
Week 2 (Epoch 1 starts):
- Backend cron detects currentEpoch = 1
- Finalizes Epoch 0 with CEOScore snapshots
- Companies can now claim Epoch 0 rewards
- Companies continue building CEOScore for Epoch 1
Week 3 (Epoch 2 starts):
- Finalize Epoch 1, companies claim
- ... and so onIntegration
Check Claimable Rewards
import { useReadContract } from 'wagmi';
const EPOCH_DIST = '0x9f96314b2c2b1Cbb7e104D50f7ce5686775D4759';
const { data: claimable } = useReadContract({
address: EPOCH_DIST,
abi: epochDistributorAbi,
functionName: 'getClaimable',
args: [epochNumber, companyAddress],
});Claim Rewards
import { useWriteContract } from 'wagmi';
const { writeContract } = useWriteContract();
await writeContract({
address: EPOCH_DIST,
abi: epochDistributorAbi,
functionName: 'claimReward',
args: [epochNumber, companyAddress],
dataSuffix: '0x6263375f376e69346a756a39',
});Batch Claim Multiple Epochs
await writeContract({
address: EPOCH_DIST,
abi: epochDistributorAbi,
functionName: 'batchClaim',
args: [[0n, 1n, 2n], companyAddress], // claim epochs 0, 1, 2
dataSuffix: '0x6263375f376e69346a756a39',
});Get Epoch Info
const { data: epochInfo } = useReadContract({
address: EPOCH_DIST,
abi: epochDistributorAbi,
functionName: 'getEpochInfo',
args: [epochNumber],
});
// epochInfo.finalized, epochInfo.rewardPool, epochInfo.totalScore, etc.Security
- Oracle authorization — only the designated oracle address can call
finalizeEpoch() - Epoch ordering — cannot finalize an epoch before it has ended
- Idempotent — double finalization reverts with
EpochAlreadyFinalized() - Double-claim prevention — claiming an already-claimed allocation reverts with
AlreadyClaimed() - ReentrancyGuard on all state-changing functions
- CEI pattern — claimed flag set before external
runToken.mint()call
Related
- $RUN Token — the reward token minted each epoch
- Revenue Engine — how protocol fees fund the burn
- Staking — stake earned $RUN to get $CEO
- $CEO Token — governance token earned from staking
- CeosCard — access credential for company deployment