Skip to content

Repository files navigation

RedDuck

Point system

An on-chain points system for a token presale:
balances live in PDA accounts, read directly via RPC — no indexer, no backend


What this is

A points system where the balance itself — not a database row, not a cached API response — is the source of truth. A frontend reads a wallet's points with one getAccountInfo call, the same way it reads a native stake account. Four pieces:

  • Program (programs/points-system/) — the Anchor program. Daily claims, a SOL-balance bonus, referrals, and social-account claims, all writing to one PDA per wallet; a Merkle-based claim for settling points into a real token later.
  • SDK (packages/sdk/) — thin TypeScript client, shared by the frontend and the verifier. PDA derivation, direct-read helpers, and a wrapper over the generated Anchor Program client for the write path.
  • Verifier (apps/verifier/) — the one off-chain piece, and it's optional: Discord/Twitter OAuth, because that's the one thing that structurally can't be checked inside a Solana program. Node + Express + Postgres.
  • Frontend (apps/web/) — the dApp that drives the program: connect a wallet, see points/streak, run every claim.

Everything that can be verified inside the program without external data is: daily-claim timing reads the Clock sysvar, the SOL bonus reads the caller's own wallet in the same instruction, referrals are bound once directly on a PDA with no off-chain registry. Social claims are the sole exception — verified via an operator-signed Ed25519 message, checked on-chain through instruction introspection rather than trusted from the client.

Program (Anchor)

Anchor program in programs/points-system/, workspace member points-system.

Instruction Role
claim_daily Clock-gated, once per UTC calendar day. Extends the streak on a consecutive day, resets it on a missed one, adds a bonus every 7th day.
claim_sol_balance One-time bonus for holding a minimum SOL balance, read from the caller's own wallet already in the instruction's account list.
connect_referral Bind-once, modeled on Drift Protocol's UserStats.referrer — the referrer is written into the referred user's own PDA the first time only, and referrer_points not being init_if_needed means Anchor's own deserialization rejects an unbound wallet as a referrer.
claim_social Verifies an Ed25519 signature via instruction introspection before crediting a Twitter/Discord connection — the only instruction with an external dependency.
submit_merkle_root / claim_tokens Cumulative-leaf Merkle claim for settling points into a real token, modeled on Saber's merkle-distributor / the Jito fork's pattern, reimplemented rather than copied (both are GPL/AGPL).

Each season gets its own isolated MerkleDistributor PDA, derived from the season number itself. UserPointsState is a single PDA per wallet, derived from the wallet address; every instruction that touches it uses init_if_needed against those same seeds, so the account is created automatically on whichever instruction a wallet happens to call first.

Built with avm, pinned to anchor_version = "1.1.2" in Anchor.toml.

anchor build
cargo test

SDK

TypeScript package in packages/sdk/, @points-system/sdk.

File Role
pda.ts PDA derivation, mirrors the Rust seeds exactly.
read.ts getUserPoints/getGlobalState/getMerkleDistributor — raw getAccountInfo + Borsh decode. No wallet, no provider, no indexer.
social.ts buildSocialClaimMessage — the message-byte-layout contract shared by the verifier (what to sign) and the client (what to submit).
client.ts PointsClient, one method per instruction, over the generated Anchor Program client.
anchor build   # regenerates target/idl, which packages/sdk/src/idl.ts imports statically
yarn workspace @points-system/sdk run typecheck

Verifier

Node + Express + Postgres in apps/verifier/, @points-system/verifier.

Route Role
GET /auth/:provider Starts the OAuth flow, redirects to Discord/Twitter's authorize page.
GET /auth/:provider/callback Exchanges the code, checks Postgres for cross-wallet dedup on that social account, signs buildSocialClaimMessage with the operator key, hands it back to the opener window via postMessage.
GET /health { discord, twitter } — which providers are actually configured. The frontend's socials panel only renders for what this reports live.

Discord and Twitter are independently optional — set credentials for either, both, or neither. See apps/verifier/README.md for creating the OAuth apps.

createdb points_verifier
cd apps/verifier
cp .env.example .env   # OPERATOR_SECRET_KEY same as a wallet export
yarn dev

Frontend

Vite + React SPA in apps/web/, @points-system/web. Connect a wallet, see points/streak, one panel per claim. Never assumes a transaction succeeded from local state — every action re-fetches the real account after confirming, and the socials panel simply doesn't render unless the verifier reports a provider configured. See apps/web/README.md for the full breakdown.

cd apps/web
cp .env.example .env.local
yarn dev

License

MIT © RedDuck Limited

About

On-chain Solana points system for a token presale: PDA balances, daily streaks, referrals, OAuth-verified social claims, and Merkle settlement into tokens.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages