Spring 2026 Campaign
Per-wallet 500 USDT bonus, redemption window ends 2026-06-10 23:59 Asia/Shanghai, 7-day per-user validity from grant moment.
This page is the live runbook for the Spring 2026 campaign. For the underlying system see Overview; for endpoint reference see User API / Admin API.
Campaign Parameters
| Item | Value |
|---|---|
| Per-wallet cap | 500 USDT (one-time, not stackable) |
| Redemption deadline | 2026-06-10 23:59:59 Asia/Shanghai (2026-06-10T15:59:59Z) |
| Per-user expiry | 7 days from grant moment |
| Latest global expiry | 2026-06-17 23:59:59 Asia/Shanghai (last grants from the deadline) |
| Asset use | U-margined perpetual futures collateral |
| Withdrawable principal | No — bonus principal is never withdrawable |
| Profit handling | Profits from bonus trades credit user principal and are withdrawable |
| Leverage | max_leverage set per grant (default matches platform config) |
| 60% net-direction rule | When bonus / total_available > 60%, new orders must match the current net position direction |
| Cost split | 50 / 50 between bonus and principal while both are positive |
Timeline
| Date (Asia/Shanghai) | Event |
|---|---|
| 2026-05-12 | Code generation + internal testing |
| 2026-05-13 → 2026-06-10 | Public redemption window (29 days) |
| 2026-06-10 23:59:59 | Codes hit ttl_days cutoff — no new grants accepted |
| 2026-06-17 23:59:59 | Last cohort's 7-day expiry ends |
| 2026-06-18 | Final reconcile-report + cleanup /admin/recall sweep |
Ops Setup
1. Backend env
BONUS_ADMIN_API_KEY=<32+ byte random; never commit>
BONUS_POOL_ADDRESS=<bonus pool wallet>
BONUS_POOL_CAP_USDT=1500000 # 3000 wallets × 500
After editing .env, restart the backend with bash start.sh <image> — docker restart does not reload env_file.
2. Migration sanity
SELECT tablename FROM pg_tables
WHERE schemaname='public' AND tablename LIKE 'bonus_%'
ORDER BY tablename;
-- expect 9 tables (8 ledger + 1 watermark)
If any table is missing, run the migration manually — the deploy pipeline does not invoke sqlx migrate run automatically.
3. Background worker sanity
docker logs --since 30m primit-backend 2>&1 \
| grep -iE 'bonus::(trade_listener|order_listener|balance_listener|funding_poll|pnl_poll|expiry|reconcile)' \
| head -30
Expect 7 started markers — one per task.
Distribution: 3000 codes of 500 USDT each
The backend requires generate-codes to reference an existing grant_batch_id, while grant-batch always immediately activates every wallet in recipients (recipients may not be empty). So the canonical two-step recipe for a pure code drop is:
Step 1 — Create a batch using an internal placeholder
KEY="$BONUS_ADMIN_API_KEY"
OP="0x<ops-operator-wallet>"
PLACEHOLDER="0x000000000000000000000000000000000000dead" # burn or internal-only wallet
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-H "Content-Type: application/json" \
-X POST https://api.primit.io/api/v1/bonus/admin/grant-batch \
-d "{
\"batch_name\": \"spring2026\",
\"grant_tier\": \"COMMUNITY\",
\"per_address_amount\": \"500\",
\"max_leverage\": 50,
\"recipients\": [\"$PLACEHOLDER\"],
\"operator_addr\": \"$OP\",
\"notes\": \"Spring 2026 placeholder\",
\"request_id\": \"spring2026-batch-001\"
}" | tee /tmp/batch.json | jq .
BATCH_ID=$(jq -r .grant_batch_id /tmp/batch.json)
Step 2 — Generate 3000 codes against that batch
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-H "Content-Type: application/json" \
-X POST https://api.primit.io/api/v1/bonus/admin/generate-codes \
-d "{
\"grant_batch_id\": \"$BATCH_ID\",
\"amount\": \"500\",
\"count\": 3000,
\"ttl_days\": 29,
\"operator_addr\": \"$OP\",
\"request_id\": \"spring2026-codes-001\"
}" | jq '. | {audit_id, codes_count: (.codes | length), replayed}'
# Export to CSV for distribution
jq -r '.codes[]' /tmp/codes.json > spring2026-codes.csv
count is capped at 5000 per call. To mint more, repeat with fresh request_ids.
Step 3 — Clean up the placeholder
The placeholder wallet was activated by Step 1 (it now holds 500 USDT bonus). Recall it:
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-H "Content-Type: application/json" \
-X POST https://api.primit.io/api/v1/bonus/admin/recall \
-d "{
\"target_address\": \"$PLACEHOLDER\",
\"reason\": \"cleanup placeholder after batch creation\",
\"operator_addr\": \"$OP\",
\"request_id\": \"cleanup-placeholder-001\"
}" | jq .
Alternative: skip codes, directly activate a known KOL list
If you already have all 3000 KOL wallets, skip code minting entirely:
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-H "Content-Type: application/json" \
-X POST https://api.primit.io/api/v1/bonus/admin/grant-batch \
-d '{
"batch_name": "spring2026-direct",
"grant_tier": "KOL",
"per_address_amount": "500",
"max_leverage": 50,
"recipients": ["0xKOL1...", "0xKOL2...", "..."],
"operator_addr": "0xOP...",
"request_id": "spring2026-direct-001"
}' | jq '.created, .failed'
Per call limit: 500 recipients. Split larger lists across multiple request_ids. Inspect .failed[] for duplicates (already_has_bonus).
User Distribution Template (zh-Hans)
🎁 Primit Spring 2026 体验金开放领取
您获得了 500 USDT 体验金,可用于 U 本位永续合约交易:
兑换截 止:2026-06-10 23:59(北京时间)
兑换代码:<填这里>
一键领取:https://app.primit.io/lighter/bonus/redeem?code=<填这里>
规则简要:体验金到账后 7 天内有效;可作为保证金开仓,
赚到的利润全部归您并可提现;体验金本金不可提现;
做空 / 做多自由选择。
The redeem page reads ?code= from the URL and pre-fills the form, so the link is a single-click experience.
Monitoring
Daily reconcile-report
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-X GET https://api.primit.io/api/v1/bonus/admin/reconcile-report | jq .
Healthy invariants the caller verifies:
net_outflow == outstanding_shadow_total(within ε)net_outflow <= pool_cap_usdt
The endpoint is read-only. The 15-minute background worker runs richer invariants and may transition accounts to frozen on its own; observe via logs:
docker logs --since 1h primit-backend 2>&1 | grep -iE 'bonus::reconcile' | tail -30
Campaign consumption SQL
-- Codes used so far
SELECT count(*) AS used, sum(amount) AS usdt
FROM bonus_codes
WHERE grant_batch_id = (SELECT id FROM bonus_grant_batches WHERE batch_name = 'spring2026')
AND status = 'redeemed';
-- Active outstanding bonus
SELECT count(*) AS active_users, sum(bonus_balance)::numeric(20,2) AS outstanding_usdt
FROM bonus_accounts
WHERE status = 'active';
-- Cost attribution by event type, last 24h
SELECT event_type,
count(*) AS events,
sum(bonus_share)::numeric(20,2) AS bonus_consumed
FROM bonus_cost_attribution
WHERE occurred_at > now() - interval '24 hours'
GROUP BY event_type
ORDER BY 3 DESC;
Real-time logs
docker logs --since 1h primit-backend 2>&1 \
| grep -iE 'bonus::(activate|recall|reconcile|expiry|freeze)' \
| tail -30
Emergency SOPs
Freeze a single account
WALLET="0xAAA..."
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-H "Content-Type: application/json" \
-X POST https://api.primit.io/api/v1/bonus/admin/freeze \
-d "{
\"target_address\": \"$WALLET\",
\"reason\": \"suspected abuse\",
\"operator_addr\": \"$OP\",
\"request_id\": \"freeze-$(uuidgen)\"
}"
Recall after adjudication
curl -sS -H "X-Bonus-Admin-Key: $KEY" \
-H "Content-Type: application/json" \
-X POST https://api.primit.io/api/v1/bonus/admin/recall \
-d "{
\"target_address\": \"$WALLET\",
\"reason\": \"campaign-end-cleanup\",
\"operator_addr\": \"$OP\",
\"request_id\": \"manual-recall-$(uuidgen)\"
}"
There is no force flag — bonus locked in margin can only be released by closing the underlying positions.
Pause the campaign without taking the service down
Expire all unused codes immediately:
UPDATE bonus_codes
SET expires_at = now()
WHERE grant_batch_id = (SELECT id FROM bonus_grant_batches WHERE batch_name = 'spring2026')
AND status = 'unused';
Existing users continue to follow their own 7-day per-user clock.
Kill switch (stop all withdrawals)
Set WITHDRAWALS_PAUSED=true in .env, then bash start.sh <image>. This is a system-wide switch and affects non-bonus users — coordinate before flipping.
User FAQ
Q: Can I withdraw the bonus itself? A: No. Bonus principal is never withdrawable. Profits earned with bonus credit your principal and are withdrawable.
Q: What happens after 7 days if I still have positions open?
A: Your account enters expired_pending: open orders are cancelled, you can only close positions. Bonus inside margin is released back to BonusPool on close. After a 7-day grace window, any still-locked bonus positions are force-liquidated by the expiry sweeper.
Q: What if I close all positions before expiry?
A: Free bonus is auto-recalled to the pool; your account becomes recalled. Profits already realized stay in your principal.
Q: Can I redeem a second code?
A: No — one bonus per wallet per campaign. The endpoint returns code_not_redeemable or already_has_bonus.
Q: Does multi-account farming work?
A: Risk controls run on KYC, device fingerprint, funding history, and counterparty graph. Detection results in freeze + recall across all linked wallets.