Airdrop API Reference
Version: 1.0 Date: 2026-04-06 Canister: airdrop Staging Canister ID: xtt27-syaaa-aaaao-batwq-caiCandid Interface: airdrop.did
Overview
The airdrop canister distributes DOM tokens to verified DAO members after a mandatory 72-hour waiting period. It enforces five gates before a claim succeeds and exposes governance controls so the DAO can pause distribution, resume it, or adjust the per-claim amount without a canister upgrade.
Key Features
- 5-Gate Validation: Active SBT, 72-hour activation wait, not yet claimed, not paused, pool funded
- One Claim per Member: Each principal can claim exactly once; subsequent calls are rejected
- Governance Kill Switch:
pause_airdrop(),resume_airdrop(),set_airdrop_amount()require governance-level authorization - Pool Balance Visibility: Anyone can query the current pool balance and total claims issued
- Upgrade Safety: Claim records and config survive canister upgrades
Claim Gates
All five conditions must pass for claim_airdrop() to succeed:
| Gate | Check |
|---|---|
| 1 — Active SBT | Caller's MembershipMetadata.status == Active |
| 2 — 72-hour wait | activated_at + 259_200_000_000_000 ns ≤ ic_cdk::api::time() |
| 3 — Not claimed | Caller principal not in claimed_by set |
| 4 — Not paused | paused == false |
| 5 — Pool funded | pool_balance >= airdrop_amount |
Authentication
All update methods use ic_cdk::caller(). Query methods are public.
Access Levels
| Level | Who |
|---|---|
| Public | Anyone (query methods, health) |
| Member | Active DAO member with SBT (claim_airdrop) |
| Governance | Governance canister principal (pause_airdrop, resume_airdrop, set_airdrop_amount) |
| Admin | Canister controller (set_governance_principal, top_up_pool) |
Data Types
ClaimStatus
Result of get_claim_status() or get_my_claim().
type ClaimStatus = variant {
NotEligible : text; // Reason string
EligibleNotClaimed; // Can claim now
Claimed : record {
tx_index : nat64;
claimed_at : nat64; // nanoseconds
amount : nat;
};
PendingActivation : record {
activated_at : nat64;
eligible_at : nat64; // activated_at + 72h
};
};Methods
claim_airdrop
Claim the airdrop distribution for the calling member.
Signature:
claim_airdrop : () -> (variant { Ok : nat64; Err : text });Access: Active members only
Returns: Result<nat64, text> — DOM token transaction index on success
Example:
const result = await airdrop.claim_airdrop();
if ('Ok' in result) {
console.log(`Claimed! Transaction: ${result.Ok}`);
} else {
console.error(`Claim failed: ${result.Err}`);
}Possible errors:
{ Err: "No active SBT found for caller" }
{ Err: "72-hour activation wait has not elapsed" }
{ Err: "Already claimed" }
{ Err: "Airdrop is paused" }
{ Err: "Insufficient pool balance" }Flow:
- Fetch
MembershipMetadatafor caller frommembershipcanister - Verify
status == Active - Verify
activated_at + 72h ≤ now - Verify caller not in
claimed_byset - Verify
paused == false - Fetch
dom-tokenpool balance and verify>= airdrop_amount - Call
dom-token.icrc1_transfer(caller, airdrop_amount) - Record claim (principal, tx_index, claimed_at)
- Return
Ok(tx_index)
get_claim_status
Check the claim status for any principal.
Signature:
get_claim_status : (who : principal) -> (ClaimStatus) query;Access: Public
Parameters:
who: Principal to check
Example:
const status = await airdrop.get_claim_status(Principal.fromText("..."));
if ('Claimed' in status) {
console.log(`Claimed at ${status.Claimed.claimed_at}, tx ${status.Claimed.tx_index}`);
} else if ('EligibleNotClaimed' in status) {
console.log('Ready to claim!');
} else if ('PendingActivation' in status) {
const eligibleAt = new Date(Number(status.PendingActivation.eligible_at / 1_000_000n));
console.log(`Eligible at ${eligibleAt.toISOString()}`);
} else {
console.log(`Not eligible: ${status.NotEligible}`);
}get_my_claim
Convenience method — same as get_claim_status for ic_cdk::caller().
Signature:
get_my_claim : () -> (ClaimStatus) query;Access: Public
get_pool_balance
Query how many DOM tokens remain in the airdrop pool.
Signature:
get_pool_balance : () -> (nat) query;Access: Public
Returns: Pool balance in smallest DOM units (8 decimals)
Example:
const balance = await airdrop.get_pool_balance();
const dom = Number(balance) / 1e8;
console.log(`Pool: ${dom.toFixed(2)} DOM`);get_claim_count
Total number of successful claims since deployment.
Signature:
get_claim_count : () -> (nat64) query;Access: Public
is_paused
Query whether the airdrop is currently paused.
Signature:
is_paused : () -> (bool) query;Access: Public
pause_airdrop
Pause all new claims. Existing claims are unaffected.
Signature:
pause_airdrop : () -> (variant { Ok; Err : text });Access: Governance canister only
Example:
// Called from governance canister via inter-canister call
const result = await airdrop.pause_airdrop();
// { Ok: null }resume_airdrop
Resume claims after a pause.
Signature:
resume_airdrop : () -> (variant { Ok; Err : text });Access: Governance canister only
set_airdrop_amount
Update the per-claim DOM amount without a canister upgrade.
Signature:
set_airdrop_amount : (amount : nat) -> (variant { Ok; Err : text });Access: Governance canister only
Parameters:
amount: New per-claim amount in smallest DOM units
Example:
// Set to 100 DOM
await airdrop.set_airdrop_amount(10_000_000_000n);Error Handling
| Error String | Cause |
|---|---|
"No active SBT found for caller" | Caller is not an active member or has no SBT |
"72-hour activation wait has not elapsed" | Member joined < 72 hours ago |
"Already claimed" | This principal already claimed once |
"Airdrop is paused" | Governance paused the airdrop |
"Insufficient pool balance" | Pool needs to be topped up |
"Governance only" | Caller is not the governance canister |
"Admin only" | Caller is not the canister controller |
Frontend Integration
import { Actor, HttpAgent } from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
const agent = new HttpAgent({ host: 'https://ic0.app' });
const airdrop = Actor.createActor(idlFactory, {
agent,
canisterId: 'xtt27-syaaa-aaaao-batwq-cai',
});
// Check eligibility before claiming
async function checkAndClaim() {
const status = await airdrop.get_my_claim();
if ('EligibleNotClaimed' in status) {
const result = await airdrop.claim_airdrop();
if ('Ok' in result) {
console.log(`Airdrop claimed! TX: ${result.Ok}`);
}
} else if ('PendingActivation' in status) {
const eligibleAt = new Date(Number(status.PendingActivation.eligible_at / 1_000_000n));
console.log(`Check back after ${eligibleAt.toLocaleString()}`);
} else if ('Claimed' in status) {
console.log('Already claimed');
}
}Related Documentation
Changelog
Version 1.0 (2026-04-06)
- Initial API documentation
- All 9 canister methods documented
- 5-gate validation logic explained
- Frontend integration example included
- ClaimStatus type documented
Last Updated: 2026-04-06 API Version: 1.0 Maintainer: API Team