pay-rewards.ts

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-wasm npm 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

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:

  1. 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.

  2. 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.