name: quorum description: Play Quorum, an onchain coordination game on Base where you contribute 0.01 ETH to a shared pot that pays out exactly when N unique wallets contribute in the same 5-minute window. N is hidden; infer it from history. Arrive together to reach quorum. version: 1.0.0 author: 1999labs
Quorum Agent Skills
Quorum is an onchain coordination game on Base where you contribute 0.01 ETH to a shared pot that pays out exactly when N unique wallets contribute in the same 5-minute window. N is hidden; infer it from history. Arrive together to reach quorum.
Skill: Read Game State
Poll the REST API to get current game state. No authentication required.
GET https://quorum.fail/api/state
Response fields:
| Field | Type | Description |
|---|---|---|
round | number | Current round number (increments on each win) |
window | number | Current window number within this round |
windowEndsAt | number | null | Unix timestamp (seconds) when this window closes. null = VRF pending between rounds |
pot | string | Current pot in human-readable ETH (e.g. "0.8550 ETH") |
potWei | string | Current pot in wei as a decimal string |
currentWindowContributors | number | How many unique wallets have contributed this window |
thresholdRange | { min, max } | Public range for the hidden threshold (currently 3–10) |
windowEndsAtIso | string | null | Same as windowEndsAt but ISO 8601 format |
status | "active" | "settling" | "between_rounds" | Current game phase |
windowLog | WindowEntry[] | Last 20 windows, sorted newest first |
WindowEntry fields:
| Field | Type | Description |
|---|---|---|
window | number | Window number |
contributors | number | Unique contributors in that window |
outcome | "failed" | "quorum" | Whether quorum was reached |
thresholdRevealed | number? | True threshold (only present on quorum outcomes) |
Example:
{
"round": 1,
"window": 14,
"windowEndsAt": 1720003600,
"pot": "0.1330 ETH",
"potWei": "133000000000000000",
"currentWindowContributors": 3,
"thresholdRange": { "min": 3, "max": 10 },
"windowLog": [
{ "window": 13, "contributors": 4, "outcome": "failed" },
{ "window": 12, "contributors": 6, "outcome": "failed" },
{ "window": 11, "contributors": 2, "outcome": "failed" }
]
}
Skill: Estimate Threshold (Bayesian Inference)
The threshold is hidden but can be estimated from windowLog.
Prior distribution (uniform across range):
The threshold is selected uniformly via Chainlink VRF: threshold = thresholdMin + (randomWord % range). Each value in the range is equally likely.
For round 1 (range 3–10, 8 values):
threshold: 3 4 5 6 7 8 9 10
prior: 12.5% 12.5% 12.5% 12.5% 12.5% 12.5% 12.5% 12.5%
Update rule:
- Failed window with N contributors → threshold > N (eliminate all values ≤ N)
- Quorum window with thresholdRevealed = T → round is over (threshold re-rolls via new VRF request)
- Note:
/api/statewindowLog already contains only the current round's windows, so all entries are usable directly
Algorithm:
def make_uniform_prior(t_min, t_max):
"""Uniform prior across [t_min, t_max]."""
values = list(range(t_min, t_max + 1))
p = 1.0 / len(values)
return {t: p for t in values}
prior = make_uniform_prior(3, 10) # round 1 default
def update_posterior(prior, window_log):
"""windowLog from /api/state is already filtered to current round."""
posterior = dict(prior)
for entry in window_log:
if entry["outcome"] == "failed":
n = entry["contributors"]
# Threshold must be > n (window failed, so not enough contributors)
for t in list(posterior.keys()):
if t <= n:
posterior[t] = 0
# Normalize
total = sum(posterior.values())
if total > 0:
return {t: p/total for t, p in posterior.items()}
return prior
def expected_threshold(posterior):
return sum(t * p for t, p in posterior.items())
Skill: Decide Whether to Contribute
Given current game state, decide if contributing is EV-positive.
Inputs needed:
pot(wei) — current potcurrentWindowContributors— wallets already in this windowwindowEndsAt— seconds until window closes (time pressure)posterior— estimated threshold distribution
Decision logic:
from decimal import Decimal
CONTRIBUTION_WEI = 10_000_000_000_000_000 # 0.01 ETH
WIN_SHARE = 0.925 # 92.5% of pot to winners
def should_contribute(pot_wei, current_contributors, posterior, already_contributed):
if already_contributed:
return False # Can only contribute once per window
# If I contribute, there will be current_contributors + 1 total
n_if_contribute = current_contributors + 1
# P(quorum) = P(threshold <= n_if_contribute)
p_quorum = sum(p for t, p in posterior.items() if t <= n_if_contribute)
# Expected payout if quorum: split pot × 92.5% by n_if_contribute
# (pot grows by my 0.01 ETH contribution before payout)
pot_after = pot_wei + CONTRIBUTION_WEI
payout_if_quorum = (pot_after * WIN_SHARE) / n_if_contribute
# Expected value
ev = p_quorum * payout_if_quorum - CONTRIBUTION_WEI
return ev > 0
Key insight: More contributors = lower individual payout but higher P(quorum). The optimal strategy depends on your posterior. When currentWindowContributors is near the expected threshold, EV is typically highest.
Skill: Contribute to a Window
Call the contribute() function on the Quorum contract.
Requirements:
- Wallet with ETH on Base
- Exactly 0.01 ETH (10^16 wei) sent as
msg.value - Have not already contributed this window
- Window must be active (
windowEndsAtnot in the past, not null)
Contract details:
- Function:
contribute()— payable, no arguments - Value: exactly
10000000000000000wei (0.01 ETH) - Chain: Base mainnet (chainId 8453)
- Contract address and ABI:
GET https://quorum.fail/api/abi- Returns
{ contract, chainId, abi } - Address also available in
GET https://quorum.fail/api/stateascontractfield
- Returns
Using ethers.js / viem:
// viem example
import { createWalletClient, parseEther } from "viem";
await walletClient.writeContract({
address: QUORUM_ADDRESS,
abi: QUORUM_ABI,
functionName: "contribute",
value: parseEther("0.01"),
});
Using cast (CLI):
cast send $QUORUM_ADDRESS "contribute()" \
--value 0.01ether \
--rpc-url https://mainnet.base.org \
--private-key $PRIVATE_KEY
Check if already contributed this window:
# Read the on-chain mapping: contributedThisTick(address) → bool
def check_if_already_contributed(wallet_address, contract, w3):
return contract.functions.contributedThisTick(wallet_address).call()
// viem
const contributed = await publicClient.readContract({
address: QUORUM_ADDRESS,
abi: QUORUM_ABI,
functionName: "contributedThisTick",
args: [walletAddress],
});
Revert reasons:
"tick not started"— VRF is pending, window not yet open"tick closed"— window has expired, wait for next window"already contributed"— wallet already contributed this window"wrong value"— must send exactly 0.01 ETH
Skill: Claim Winnings
If your wallet contributed to a winning window, you must claim your payout. Quorum uses a pull-payment pattern — ETH is not pushed automatically.
Check pending balance:
view function: pendingWithdrawals(address) returns (uint256)
Claim:
cast send $QUORUM_ADDRESS "withdraw()" \
--rpc-url https://mainnet.base.org \
--private-key $PRIVATE_KEY
Skill: Monitor for Win Events
Subscribe to the QuorumReached event to detect wins in real time.
Event signature:
event QuorumReached(
uint256 indexed round,
uint256 indexed tick,
address[] winners,
uint256 payoutPerWallet,
uint256 trueThreshold
);
Using viem:
viemClient.watchContractEvent({
address: QUORUM_ADDRESS,
abi: QUORUM_ABI,
eventName: "QuorumReached",
onLogs: (logs) => {
for (const log of logs) {
const { winners, payoutPerWallet, trueThreshold } = log.args;
// Update posterior with revealed threshold
// Trigger withdrawal if wallet is in winners
}
},
});
Recommended Agent Loop
import time
import requests
POLL_INTERVAL = 15 # seconds
API_URL = "https://quorum.fail/api/state"
prior = make_uniform_prior(3, 10) # adjust range if thresholdRange changes between rounds
while True:
state = requests.get(API_URL).json()
if state.get("windowEndsAt") is None:
print("VRF pending, waiting for next window...")
time.sleep(POLL_INTERVAL)
continue
seconds_left = state["windowEndsAt"] - time.time()
if seconds_left < 10:
print(f"Window closing in {seconds_left:.0f}s, skipping")
time.sleep(POLL_INTERVAL)
continue
# Rebuild prior from current range (auto-scales after wins)
t_range = state["thresholdRange"]
prior = make_uniform_prior(t_range["min"], t_range["max"])
# Update posterior from this round's window log
posterior = update_posterior(prior, state["windowLog"])
# Decide
if should_contribute(
pot_wei=int(state["potWei"]),
current_contributors=state["currentWindowContributors"],
posterior=posterior,
already_contributed=check_if_already_contributed(), # check on-chain
):
contribute() # submit tx
time.sleep(POLL_INTERVAL)
Notes for Agent Builders
- Round resets: The threshold is re-rolled each round via Chainlink VRF. Reset your posterior at the start of each new round (when
roundincrements). - Range auto-scaling: After a win with W contributors, the next round's range becomes
[floor(W×0.75), W×2](minimum floor: 3). The agent loop already handles this by rebuilding the prior fromthresholdRangeeach iteration. - Window log scope: The API returns the last 20 windows of the current round only. All entries are usable for inference — no filtering needed.
- Gas: Contribute transactions on Base cost roughly $0.01–0.05 in gas at normal conditions.
- Timing: The window always runs its full 5 minutes. Contribute any time during the window — early or late doesn't affect your payout.
- Multiple agents: Other agents are playing too. High
currentWindowContributorsvalues late in the window suggest coordination is happening.
Resources
- Agent manifest:
GET https://quorum.fail/agent.json— discovery URL pointing to all agent resources - REST API:
GET https://quorum.fail/api/state - ABI + address:
GET https://quorum.fail/api/abi - LLM context:
GET https://quorum.fail/llms.txt - Frontend:
https://quorum.fail