Space and Time
Use Case: Fairdrop

How to structure a Fairdrop

A tutorial based on Space and Time's Genesis Validator Rewards. Publish the eligibility rule as SQL, prove the result against indexed data, and deploy a claim contract from the Fairdrop factory.

Visible eligibility
The distribution rule is SQL, not a hidden snapshot script.
Proven rewards
Each claim is backed by a Proof of SQL result over indexed data.
No static snapshot
Eligibility can be recomputed from indexed activity instead of a frozen list.

Most airdrops are static lists. Someone runs a script offchain, dumps the result into a CSV, generates a Merkle tree, and ships a contract that lets eligible wallets claim. It works, but it has a problem: the eligibility logic is invisible. Users have to trust that the team's snapshot was honest and that the script did what it was supposed to.

A fairdrop flips this. Instead of distributing a precomputed list, you publish the eligibility rule itself as a SQL query, run it against indexed onchain data, and let a zero-knowledge proof guarantee the result. The rule is the source of truth. Anyone can read it, audit it, and verify their own allocation.

This post walks through how we structured the SXT Genesis Validator Rewards fairdrop using this approach, the math behind it, the smart contract that resulted, and how to replicate the pattern for your own protocol.

Where the query data comes from

A fairdrop is only as good as the data the query reads. With Space and Time, that data can come from two places, and you can combine them freely inside the same SQL.

Onchain data, indexed by SXT. You point SXT at any smart contract on a supported network, select the events you care about, and SXT indexes those events into queryable tables. Every emitted event becomes a row, with columns derived from the event signature plus standard block metadata.

Offchain data, published as an SXT table. You can also write your own tables directly to SXT Chain (CSV upload, API insert, or programmatic writes) and query them the same way. This is what the Airdrop use case does when you upload a precomputed allocation list: the CSV becomes an onchain table, and the claim contract queries it.

Both sources are stored with cryptographic commitments signed by the SXT validator set, which is what allows Proof of SQL to produce a zero-knowledge proof that a query result was computed correctly over untampered data. From the query's perspective, onchain and offchain tables look the same.

What a fairdrop unlocks

Once your eligibility rule is a SQL query against verifiable data, you can use the same primitive for a lot more than a one-time token drop.

  • Retroactive airdrops with conditional logic. Reward users who did something specific, at a specific time, and met some condition afterward. The Genesis Validator Rewards Program is exactly this: stake within 14 days, hold for 9 months, get a boosted payout.
  • Loyalty and volume-based rewards. Reward users by activity, frequency, or volume rather than a static snapshot. The query can sum trade volume, count distinct days of activity, or weight contributions over time.
  • Rewards for non-yield-bearing stablecoin holders. Stablecoins by design don't pay yield to holders, but a separate contract can. SXT covers this pattern explicitly in the User Rewards for Stablecoins writeup: index transfer and balance data, define a reward function over time-weighted holdings, and let users claim against a proven result.
  • Continuous emissions tied to verifiable activity. Because the query is re-runnable, you can structure ongoing rewards rather than a one-shot drop. New activity gets reflected the next time someone claims.

The common thread: any rule you can express in SQL over indexed data is a candidate. The infrastructure does not change between use cases. The query does.

Setting up a fairdrop correctly

A few practical things to think about before you ship one of these.

Understand the distribution of every variable your query depends on. If your eligibility rule rewards "users who staked X tokens," it helps to know what the histogram of stake sizes looks like before you pick the value of X. Pull the data first, then choose. Pick the threshold blind and you either qualify almost nobody (which underspends the budget) or almost everybody (which spends it on dust wallets). The Query Workbench is good for this: run aggregations against the indexed tables and look at the shape of the data before you commit to a rule.

Cap it. Unbounded rules drain budgets fast: one large wallet, one loop, one unexpected user pattern. Bound the payout in time, per wallet, or in total, and put the bound in the SQL.

Test that the right amounts go to the right wallets. Run the query in the workbench before deploying. Spot-check the top recipients, the bottom recipients, and a few edge cases (users who staked right before the cutoff, users who unstaked just after). If a wallet gets an amount that surprises you, either the query is wrong or your mental model of the rule is wrong. Either way, you want to find that out before the contract is deployed and immutable.

Return raw token units. The standard Fairdrop contract expects a cumulative reward amount, not a human-formatted token amount. For an 18-decimal token, one full token is 1000000000000000000. For a 6-decimal token, one full token is 1000000.

Our use case: the SXT Genesis Validator Rewards math

The Genesis Validator Rewards Program rewards early stakers who staked within the first 14 days of SXT mainnet and kept their stake active for at least 9 months. Stakers who meet both conditions get their standard ~8% annualized rewards adjusted upward to an effective ~30% annualized.

We express the entire eligibility-and-payout rule as one SQL query:

sqlgenesis-validator-rewards.sql
SELECT CAST((SUM(STAKED) * CAST(SUM(UNSTAKED) = 0 AS BIGINT)) AS DECIMAL(75, 0))
* CAST(0.217 AS DECIMAL(75,3)) AS REWARD
FROM (
SELECT CAST(SUM(AMOUNT) AS DECIMAL(75, 0)) AS STAKED, 0 AS UNSTAKED
FROM STAKING_5DHZMDXQYB8ZAAYOPX4BKSSZKMK2DSPKJX5YYAH1Q8GELVL3.STAKED
WHERE STAKER = $1
AND BLOCK_NUMBER < 22534698
UNION ALL
SELECT 0 AS STAKED, COUNT(*) AS UNSTAKED
FROM STAKING_5DHZMDXQYB8ZAAYOPX4BKSSZKMK2DSPKJX5YYAH1Q8GELVL3.UNSTAKE_INITIATED
WHERE STAKER = $1
AND BLOCK_NUMBER < 24408385
)

A few design choices worth surfacing:

  • Two cutoffs, not one. The stake snapshot marks the end of the 14-day staking window. The unstake check extends roughly 1.87M blocks (about 9 months on Ethereum) past the snapshot, covering the full lock-up window. We use two different block numbers because we are measuring two different things: "did you stake in time?" and "did you stay staked long enough?"
  • Unstaking stops eligibility. An unstake event during the genesis period voids the full reward.
  • The CAST(... = 0 AS BIGINT) trick. This is how we encode the indicator function in SQL. Booleans cast to integers give 1 or 0, and multiplying by the staked sum cleanly zeroes out ineligible wallets without a CASE branch.
View Genesis contract 0xc9a9...A480

Translating the rule into a smart contract

Here is the path from "I have a query" to "I have a deployed contract that pays out against it."

Step 1: Index your contract's events

The query above reads from two tables, STAKED and UNSTAKE_INITIATED, which correspond to events emitted by the SXT staking contract on Ethereum. Before any of this works, those events need to be indexed into queryable tables on SXT Chain.

You do this from the Index a Contract page. You point SXT at your contract address, select which events to index, and SXT spins up the tables for you. Every event emitted from the contract becomes a row in the table. The schema is derived from your event signatures, so event Staked(address staker, uint256 amount) becomes a STAKED table with STAKER and AMOUNT columns plus the standard block metadata.

Step 2: Write and test the query in the workbench

Open the Query Workbench and iterate on the SQL against the live indexed tables. Try it with several known wallets. Check the output. Vary the block cutoffs and watch the totals move the way you expect.

One thing to get right at this stage: the query needs and expects exactly one parameter, and that parameter is the claimant's wallet address. The fairdrop contract will pass the caller's address into the query at claim time as $1, so every reference to the user in the SQL (in our case, WHERE STAKER = $1 in both subqueries) has to point at that same parameter.

The deployed Fairdrop contract expects the query result to be one row and one column: a nonnegative DECIMAL(75, 0) value representing the claimant's cumulative lifetime reward in reward-token base units. The contract subtracts totalClaimed[$1] and transfers only the unclaimed delta.

This is where you confirm that the variables in your formula are doing what you think. Check how many addresses would qualify versus disqualify. Compare your total expected payout to your funding budget.

Step 3: Compile the proof plan and deploy the query contract

Once the query is final, we compile the query into a proof plan. The plan is what the prover uses to generate a ZK proof that the SQL was executed correctly, and that proof can be verified onchain. The plan is embedded in your contract at deploy time.

A user calling claim() triggers the following flow:

  1. Your fairdrop contract emits a query request for its embedded query plan, and with the caller's address as the parameter ($1 in our SQL).
  2. SXT executes the SQL against the indexed tables and returns the result plus a zero-knowledge proof.
  3. SXT sends the proof to the onchain verifier contract and, if valid, it forwards the result to the fairdrop contract
  4. Your fairdrop contract receives the verified reward amount and transfers the corresponding tokens to the user

No backend, no admin, no offchain trust assumption beyond the validator set securing the data commitments.

Prepare the inputs, then return to launch

Use the focused tools for indexing and query exploration. Then come back with the final query or compiled proof plan.

  1. 1
    Index your activity

    Point SXT at the contract events that describe the behavior you want to reward.

    Index a Contract
  2. 2
    Test the reward rule

    Use the workbench to inspect distributions, tune cutoffs, and test known wallets.

    Open Query Workbench
  3. 3
    Bring back the final query

    Paste final SQL here to compile a proof plan, or paste a proof plan you already generated.

    Go to Launcher

Connect your wallet to complete this tutorial

Launch your Fairdrop

Launch your Fairdrop contract from our factory smart contract with just your SQL and the reward token of your choice.

Query and proof plan

Your SQL must accept the claimant wallet as $1 and return exactly one DECIMAL(75, 0) column: the claimant's cumulative reward in raw token units. The starter query pays one whole 18-decimal token per matching event by multiplying the event count by 1000000000000000000; change that scale for reward tokens with different decimals.

Final SQL
Open Query Workbench
Generate or paste a valid hex proof plan before deployment.
Deployment configuration

Connected chain: Ethereum

Advanced options
Factory: 0x1d6bac431bf4278fb6b26036dc5cff8cf01af6b8
Query Router: 0x220a7036a815a1Bd4A7998fb2BCE608581fA2DbB

Connect your wallet to deploy.

Fund and share the claim page

After deployment, transfer reward-token liquidity into the Fairdrop contract. The generated claim URL uses the generic Fairdrop claim page and the deployed contract address.

Deploy from the factory first. The deployed address is read from the factory event.

For developers

The factory path is the normal launch flow, so it does not require a package download. If you need to inspect or extend the contracts, download the latest Genesis Rewards fairdrop package with the factory, query utility library, deployment scripts, and tests.

  • Factory-first path. Use the launcher above when the standard contract fits your campaign.
  • Genesis contract package. Download the latest Fairdrop, FairdropFactory, QueryUtil, interfaces, scripts, and tests.
  • Static allocation fallback. Use the Airdrop use case when the address and amount list is already final.
Static allocation instead? Open Airdrop