seeker-genesis-token

Verify Seeker device ownership by checking for the Seeker Genesis Token (SGT) with Sign-in-with-Solana and server-side token verification. Use when gating content or rewards to Seeker owners, verifying a user holds an SGT, adding anti-Sybil checks to a Solana mobile app, or implementing one-claim-pe

By solana-mobile · 4,709 installs

npx skills add solana-mobile/solana-mobile-skills --skill seeker-genesis-token

Source repository · Upstream listing

Seeker Genesis Token verification The Seeker Genesis Token (SGT) is a Token 2022 NFT minted once per Seeker device. Holding one is evidence of owning a Seeker, which makes it useful for gating rewards and for one claim per device logic. Verification has two halves, and both are required : 1. Prove the user controls the wallet — Sign in with Solana (SIWS) 2. Prove that wallet holds an SGT — inspect the wallet's Token 2022 mints Doing only the second means anyone can submit a real Seeker owner's public address and pass. Doing only the first proves wallet control but says nothing about a device. This must run on a server Never decide entitlement client side. A client can be patched, so a client side hasSGT boolean is worth nothing. The client's job is to collect a signature; the server verifies it and owns the result. Requirements: A backend you control that can make Solana mainnet RPC calls Storage for nonces and claim records An RPC endpoint. A paid provider helps, since the check may enumerate many token accounts — keep that key server side only, never in an EXPO PUBLIC variable Prerequisites A working wallet connection. If the app has none, use the solana mobile wallet skill first; this skill assumes useMobileWallet() is available. Testing needs a physical Seeker device — an emulator cannot hold an SGT. Plan for a code path you can exercise without one, such as a server side allowlist in development. Step 1: issue the payload from the server The server issues the whole SIWS payload, not just a nonce, and stores it under the nonce with a short TTL. Every field the client supplies is a field an attacker chooses, and each one is fed straight to the signature check, so pin them all at issue time. The nonce must be server generated, single use, and short lived . A client generated or reusable nonce makes the signature replayable, which defeats the exercise. address is the one field the server cannot fill in. The client adds it in step 2. chainId is pinned to solana:mainnet deliberately rather than taken from the wallet's chain : SGTs exist only on mainnet, so a signature scoped to devnet proves nothing about a device. Issuing it server side is what turns that into a guarantee instead of a comment. Step 2: sign in on the client signIn from the wallet hook authorizes and proves ownership in a single prompt. Spread the issued payload and add the address: Post exactly those four fields. Do not post output.account , and the server must never accept an account object or a public key from the client — the key the signature is checked against has to be derived from the address the server is about to act on. Step 3 shows why. nonce is only a lookup key here; the signature check is what binds it. This is the fully specified payload, not the short signIn form. domain , nonce , and version are what make the signature non replayable and bind it to your app, and the server has to have issued them for that to hold. The solana mobile wallet skill covers both forms and when each is appropriate. Step 3: verify the signature on the server The three shape checks come first because every one of those values is attacker chosen. Without them the malformed cases reach new Uint8Array() and then the ed25519 verify, where they land inconsistently: a short signature throws a library error about byte lengths, while a non array signedMessage coerces to an empty array and returns a plain false . Rejecting on shape gives one clear answer for both and does not lean on internals you do not control. verifySignIn compares the fields you pass it against the fields inside the signed text, then verifies the signature with account.publicKey . It never checks that the key and the address agree. Take that key from the request body and any throwaway keypair can sign a message naming any address: the signature is genuine, it just is not the address holder's. Because the server issued issued , domain and chainId are pinned by construction. No client copy of either reaches the check, so no separate domain comparison is needed. If you want a defensive assertion anyway, assert against issued.domain — the stored copy — never a value off the request. Step 4: check the wallet for an SGT See [references/sgt verification.md](references/sgt verification.md) for the full implementation. It confirms three properties of a Token 2022 mint — mint authority, metadata pointer, and token group membership — and all three must match. Step 5: combine the checks correctly This is where the subtle bug lives. verifySignIn does not bind the account key to the address in the signed message, so the server has to do that binding itself: derive the key from the address it is going to act on, and look the SGT up against that same address. Checking the SGT against any other address lets a caller submit a real Seeker owner's address with their own signature and be granted access. Anti Sybil: one claim per device An SGT is per device, so the mint address is the device identity. Store that, not the wallet address — a wallet can hold a different SGT later, and a device's SGT can move between wallets. A UNIQUE constraint is what enforces one claim, not application code: The insert and the payout are two separate writes, so decide what happens when the second one fails. As written, a grantReward that throws after the row commits leaves that device unable to retry: the constraint now rejects it as already claimed. Either drive retries off rows with a null paid at , or make grantReward idempotent on mintAddress so replaying it is free. This one is robustness rather than security: getting it wrong denies a real owner their reward, it does not let anyone claim twice. Have checkWalletForSGT return the mint address rather than a bare boolean — see the end of the reference file. Reference material [references/sgt verification.md](references/sgt verification.md) — full verification implementation, SGT constants, standard RPC and Helius variants Related skills solana mobile wallet — wallet connection and the two signIn payload forms seeker domains — .skr domain resolution, which Seeker users have by default Links Detecting Seeker users: https://docs.solanamobile.com/react native/detecting seeker users Sign in with Solana spec: https://github.com/phantom/sign in with solana