Skip to main content

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

ItemValue
Per-wallet cap500 USDT (one-time, not stackable)
Redemption deadline2026-06-10 23:59:59 Asia/Shanghai (2026-06-10T15:59:59Z)
Per-user expiry7 days from grant moment
Latest global expiry2026-06-17 23:59:59 Asia/Shanghai (last grants from the deadline)
Asset useU-margined perpetual futures collateral
Withdrawable principalNo — bonus principal is never withdrawable
Profit handlingProfits from bonus trades credit user principal and are withdrawable
Leveragemax_leverage set per grant (default matches platform config)
60% net-direction ruleWhen bonus / total_available > 60%, new orders must match the current net position direction
Cost split50 / 50 between bonus and principal while both are positive

Timeline

Date (Asia/Shanghai)Event
2026-05-12Code generation + internal testing
2026-05-13 → 2026-06-10Public redemption window (29 days)
2026-06-10 23:59:59Codes hit ttl_days cutoff — no new grants accepted
2026-06-17 23:59:59Last cohort's 7-day expiry ends
2026-06-18Final 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.