pay-rewards.ts
Payout script for $PIXEL KRC-20 rewards to original NFT minters.
Rewards go to the MINTER, not the current holder
For each NFT the script fetches the first history entry via:
GET /api/v1/krc721/mainnet/history/{tick}/{id}?direction=forward&limit=1
The owner field in that entry is the minter (mint destination address). Rewards
always go to this address, regardless of subsequent transfers.
Prerequisites
- Bun >= 1.0
kaspa-wasmnpm package (see below)- Treasury wallet private key (hex, without 0x prefix)
Setup
1. Install dependencies
cd <repo-root>
bun add kaspa-wasm
2. Configure .env
Copy .env.example and fill in your values:
cp .env.example .env
Required keys:
| Variable | Description |
|---|---|
PIXEL_PRIVATE_KEY |
Hex private key of the treasury wallet sending $PIXEL |
TREASURY_ADDRESS |
kaspa:q… address of that wallet (for display/verification) |
The .env file is gitignored. Never commit it.
3. WASM binary (if kaspa-wasm npm fails)
If bun add kaspa-wasm installs but the script fails at runtime with a WASM loading
error, use the manual WASM approach from coinchimp/kaspa-krc20-apps:
# Download the Kaspa WASM SDK (nodejs build)
# https://kaspa.aspectron.org/nightly/downloads/
# Extract the zip, copy the nodejs/ folder to <repo-root>/wasm/
Then change the import in pay-rewards.ts:
- kaspa = await import("kaspa-wasm");
+ kaspa = await import("../wasm/kaspa");
Usage
Dry run (recommended first)
Fetches all data, prints the payout table, does NOT send any transactions:
bun run scripts/pay-rewards.ts --dry-run
Pay both collections
bun run scripts/pay-rewards.ts
Pay a single collection
bun run scripts/pay-rewards.ts --collection SYKORA
bun run scripts/pay-rewards.ts --collection PIXELONKAS
Dry run for one collection
bun run scripts/pay-rewards.ts --dry-run --collection SYKORA
Reward table
SYKORA (trait: Rarity / Style)
| Trait | Value | $PIXEL reward |
|---|---|---|
| Rarity | Special (= Ice) | 100,000,000 |
| Style | Black | 80,000,000 |
| Style | Pink | 40,000,000 |
| other | — | 0 |
PIXELONKAS (trait: Color)
| Color | $PIXEL reward |
|---|---|
| Super Broken | 1,000,000,000 |
| Broken | 100,000,000 |
| Grey | 80,000,000 |
| Orange | 50,000,000 |
| Yellow | 30,000,000 |
| Pink | 26,000,000 |
| Red | 24,000,000 |
| Purple | 22,000,000 |
| White | 20,000,000 |
| Green | 15,000,000 |
| Black | 12,000,000 |
| Blue | 12,000,000 |
Idempotence — the chain is the record
Before paying anything, the script asks the indexer how much $PIXEL each of the payer wallets has already sent to each address, and subtracts that from what is owed. Two wallets are checked:
| Wallet | Role |
|---|---|
TREASURY_ADDRESS (from .env) |
pays rewards today |
kaspa:qqxpsvl25l2cf0zrx2wvpnulgthla5ckq9ae4rttw5mupm9e6hc0ujt8ugtre |
paid the 2025 rewards, before the treasury existed |
Anything sent back to a payer wallet is subtracted — that is how the April 2026 decimal-bug dust, which recipients returned, nets out to zero.
logs/*.json is still read, and an address’s logged total is used when it is
larger than what the chain reports. It can only raise a figure, never lower
one. Neither source is trusted alone: logs/ is gitignored and lives on one
machine, and the indexer prunes old operations, so whichever remembers more
wins.
Losing logs/ cannot cause a double payment. Verified by deleting the
directory and re-running — the payout table was identical.
It fails closed
If the indexer cannot be reached, the run aborts. It does not fall back to “nothing has been paid”, because that would re-send every reward already out. The same applies to unreadable token metadata (unknown traits mean an unknown reward) and to missing mint history (unknown minter). All three stop the run and name the tokens or wallet involved.
Point KASPLEX_API at a mirror if the default host is down:
KASPLEX_API=https://your-mirror/v1/krc20 bun run scripts/pay-rewards.ts --dry-run
Log format
logs/ keeps an audit trail, and logs/index.json — a manifest of the log
files — is rewritten on every run so admin/rewards-tracker.html can find logs
of any age.
{
"minterAddress": "kaspa:q…",
"sykoraTokens": [1181],
"pixelonkasTokens": [],
"amount": 100000000,
"txHash": "abc123…",
"timestamp": "2026-04-04T10:00:00.000Z"
}
Entries whose txHash is not a real hex transaction hash (FAILED:…,
manual-import:…) are ignored when totalling what was paid.
KRC-20 transfer mechanics
Each payout executes a two-step commit/reveal:
-
Commit — sends 0.3 KAS to a P2SH address derived from the inscription script (which encodes the KRC-20 transfer data). Costs ~0.3 KAS in gas.
-
Reveal — spends the P2SH UTXO, triggering the Kasplex indexer to process the inscription and credit the $PIXEL to the destination address. Costs another ~0.3 KAS in gas.
Total gas per payout: ~0.6 KAS. Ensure the treasury wallet has enough KAS. The $PIXEL amount comes from the treasury wallet’s KRC-20 balance.
Troubleshooting
ABORTED: could not verify prior payments
The KRC-20 indexer is unreachable, so the script cannot tell who has already
been paid. This is intentional — wait for it to come back, or set KASPLEX_API
to a working mirror. Never work around it by deleting the check.
PIXEL_PRIVATE_KEY missing in .env
Add PIXEL_PRIVATE_KEY=<hex> to your .env file.
cannot import kaspa-wasm
Run bun add kaspa-wasm. If that does not fix it, use the manual WASM approach (see Setup section).
Transfer fails with Insufficient KAS for gas
Top up the treasury wallet with KAS. Each transfer needs ~0.6 KAS for commit + reveal gas.
Timeout: commit UTXO not found
The Kaspa network may be congested or the node unresponsive. Re-run — idempotence ensures
already-paid addresses are skipped.