MCPInstall

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-per-device logic.

Installs
11.7K
Last 24h
+78
Trending rank
#2342
Security
Pass

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/solana-mobile/solana-mobile-skills --skill seeker-genesis-token
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads seeker-genesis-token automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

Security audits

Independent scans of the skill's instructions and bundled files.

Gen Agent Trust Hub PassSAFE ·The seeker-genesis-token skill provides secure implementation patterns for verifying Seeker Genesis Token ownership. It covers server-side nonce generation, wallet signature verification using Sign-in-with-Solana (SIWS), and on-chain token metadata validation. The instructions emphasize security best practices, including input sanitization, key derivation from target addresses, and atomic transaction handling for anti-Sybil protections.
Socket PassNo alerts
Snyk PassLOW ·Risk: LOW · No issues

SKILL.md

The instructions your agent reads when the skill is triggered.

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
  • A mainnet RPC endpoint. A paid provider helps, since the check reads every Token-2022 account the wallet holds — 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.

// POST /api/siws/nonce
const issuedAt = new Date()
const nonce = crypto.randomBytes(16).toString('hex')

const payload = {
  chainId: 'solana:mainnet',
  domain: 'yourdapp.com',
  expirationTime: new Date(issuedAt.getTime() + 300_000).toISOString(),
  issuedAt: issuedAt.toISOString(),
  nonce,
  statement: 'Sign in to verify Seeker ownership',
  uri: 'https://yourdapp.com',
  version: '1',
}

await store.put(nonce, payload, { ttlSeconds: 300 })
return payload

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:

import { useMobileWallet } from '@wallet-ui/react-native-kit'

const { signIn } = useMobileWallet()

const issued = await fetch('https://yourdapp.com/api/siws/nonce', { method: 'POST' }).then(
  (response) => response.json(),
)

const address = account.address.toString()
const output = await signIn({ ...issued, address })

await fetch('https://yourdapp.com/api/siws/verify', {
  body: JSON.stringify({
    address,
    nonce: issued.nonce,
    signature: Array.from(output.signature),
    signedMessage: Array.from(output.signedMessage),
  }),
  headers: { 'Content-Type': 'application/json' },
  method: 'POST',
})

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

npm install @solana/kit @solana/wallet-standard-util
import { getBase58Encoder } from '@solana/kit'
import { verifySignIn } from '@solana/wallet-standard-util'

function verifySiws(issued, { address, signature, signedMessage }) {
  // Reject malformed input here, not inside the ed25519 verify.
  if (!Array.isArray(signature) || signature.length !== 64) {
    throw new Error('Malformed signature.')
  }
  if (!Array.isArray(signedMessage)) throw new Error('Malformed signed message.')

  // Derive the verifying key from the address, never from the request body.
  const publicKey = getBase58Encoder().encode(address)
  if (publicKey.length !== 32) throw new Error('Malformed address.')

  return verifySignIn(
    { ...issued, address },
    {
      account: { address, chains: [], features: [], publicKey },
      signature: new Uint8Array(signature),
      signedMessage: new Uint8Array(signedMessage),
    },
  )
}

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

Use @solana-mobile/skr-genesis-token. Do not hand-roll the check: the package is maintained by Solana Mobile and gets the details right that a copied snippet tends to miss.

npm install @solana-mobile/skr-genesis-token @solana/kit
import { getSeekerGenesisTokens } from '@solana-mobile/skr-genesis-token'
import { address, createSolanaRpc } from '@solana/kit'

// SGTs exist only on mainnet, whatever cluster the rest of the app targets.
const rpc = createSolanaRpc(process.env.SOLANA_MAINNET_RPC_URL!)

async function getWalletSGTMints(walletAddress: string) {
  const tokens = await getSeekerGenesisTokens(rpc, address(walletAddress))
  return tokens.map((token) => token.mint)
}

Return the mint addresses, not just a boolean: a mint identifies a device, which is what the anti-Sybil check below records. Keep every mint, not only the first: one wallet can hold the SGTs of several devices, and each of them is entitled to claim. hasSeekerGenesisToken exists for a plain yes/no, and isSeekerGenesisToken verifies a single mint.

What the package handles, so you do not add it again:

  • A mint is an SGT only if both its TokenGroupMember group and its MetadataPointer are the SGT group mint. It decodes those extensions from the mint bytes, never from names or off-chain metadata.
  • Zero-balance token accounts are skipped. Transferring an SGT out leaves the old account open at 0; counting it would verify every past holder forever.
  • Frozen accounts are not rejected. A legitimately held SGT sits in a frozen account — do not add a freeze check on top.
  • RPC failures throw. Let them: map them to a 503 at the API boundary, never to an empty list, or an outage reads as "you do not own a Seeker".

It reads the wallet's Token-2022 accounts in one getTokenAccountsByOwner request, with no pagination, using standard RPC methods only, so any mainnet endpoint works.

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.

async function verifySeekerUser({ address, nonce, signature, signedMessage }) {
  // 1. Consume the nonce atomically. Read-then-mark leaves a window where two concurrent
  //    requests both see it unused, and one signature is accepted twice.
  //    Redis: GETDEL. SQL: DELETE FROM nonces WHERE nonce = $1 RETURNING payload.
  const issued = await store.consume(nonce)
  if (!issued) throw new Error('Invalid, reused, or expired nonce.')

  // 2. The signature must be valid for the payload we issued and for this address.
  if (!verifySiws(issued, { address, signature, signedMessage })) {
    throw new Error('Invalid signature.')
  }

  // 3. Check the SGT against the address the signature was just verified against.
  const mintAddresses = await getWalletSGTMints(address)

  return { address, mintAddresses }
}

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:

CREATE TABLE claims (
  claimed_at timestamptz NOT NULL DEFAULT now(),
  mint_address text NOT NULL UNIQUE,
  paid_at timestamptz
);
const { address, mintAddresses } = await verifySeekerUser(request.body)
if (mintAddresses.length === 0) throw new Error('No Seeker Genesis Token found.')

// Insert first and let the constraint decide. Asking whether a row exists and then
// inserting is a double-claim bug: two concurrent requests both find nothing.
// Try each held SGT in turn, so a wallet whose first device has claimed can claim for the next.
let mintAddress: string | undefined
for (const candidate of mintAddresses) {
  try {
    await claims.insert({ claimedAt: new Date(), mintAddress: candidate })
    mintAddress = candidate
    break
  } catch (error) {
    // 23505 is the Postgres unique violation. Other drivers report it differently.
    if ((error as { code?: unknown } | null)?.code !== '23505') throw error
  }
}
if (!mintAddress) throw new Error('Every Seeker in this wallet has already claimed.')

// Pay out only once the insert has landed. Anything paid before it can be paid twice.
await grantReward(address)
await claims.markPaid(mintAddress)

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.

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

FAQ

What does the seeker-genesis-token skill do?

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-per-device logic.

How do I install seeker-genesis-token?

Run npx skills add https://github.com/solana-mobile/solana-mobile-skills --skill seeker-genesis-token in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/seeker-genesis-token/ (personal) or .claude/skills/seeker-genesis-token/ (project).

Which AI agents can use seeker-genesis-token?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is seeker-genesis-token safe to install?

Security partners have reviewed it: Gen Agent Trust Hub passed; Socket passed; Snyk passed. As with any skill, read the SKILL.md and bundled scripts before installing.

Who maintains seeker-genesis-token?

It is published by solana-mobile in solana-mobile/solana-mobile-skills. It has 11,721 recorded installs.

More from solana-mobile/solana-mobile-skills

View all →

solana-mobile

solana-mobile/solana-mobile-skills

Scaffold, configure, and troubleshoot Solana Mobile apps for Android using the solana-mobile CLI, Expo, and React Native. Use when creating a new Solana mobile app, picking a template, adding Solana to an existing Expo app, checking the Android toolchain, managing Android emulators or connected devices, installing a wallet APK for testing, running a local validator the device can reach, or producing a development build.

Mobile11.7K installs

solana-mobile-wallet

solana-mobile/solana-mobile-skills

Connect Solana wallets and sign or send transactions in React Native Expo apps using Mobile Wallet Adapter and Wallet UI. Use when adding a connect wallet button, showing a connected address, disconnecting, signing messages, sign-in with Solana, transferring SOL, or sending any transaction from a Solana mobile app.

Mobile11.7K installs

seeker-domains

solana-mobile/solana-mobile-skills

Resolve and display .skr domain names in Solana mobile apps, in both directions between names and wallet addresses. Use when showing .skr names instead of wallet addresses in profiles, friend lists, or transaction history, resolving a .skr domain to an address, reverse-looking-up an address to a domain, or validating .skr input.

Mobile11.7K installs

integration-privy

solana-mobile/solana-mobile-skills

Add Privy authentication to a Solana Expo Android app on top of Mobile Wallet Adapter, using Sign-In-With-Solana. Use when installing @privy-io/expo, mounting PrivyProvider, logging a user in with useLoginWithSiws, linking a wallet to an existing Privy account, reading the Privy access token from a backend, or debugging a Privy plus MWA setup.

Mobile11.7K installs

seeker-connect

solana-mobile/solana-mobile-skills

Connect a web, React Native, or Kotlin dapp to the Seeker device's built-in wallet with Seeker Connect. Use when adding wallet connection, sign-in with Solana, message signing, or transaction signing to an Expo or React Native app on a Seeker phone, a Kotlin app on it, or a website that runs in its browser, registering the "Seeker Connect" Wallet Standard wallet, setting the Nostr relay for relayDomain, using the seeker-connect-button element or the SeekerConnectButton native view, installing @solana-mobile/seeker-connect-react-native or com.solanamobile:seeker-connect, or debugging SeekerConnectError codes like association-failed.

Mobile7.1K installs

solana-mobile-publishing

solana-mobile/solana-mobile-skills

Build, sign, and publish an Android APK to the Solana dApp Store with the dapp-store CLI. Use when shipping a Solana mobile app to the dApp Store for the first time, releasing an update, signing a release APK, setting up a publisher account and App NFT, or debugging a release build that works fine in debug.

Mobile6.8K installs

Popular mobile skills

View category →

design-mobile-apps

designed-by-ai/skills

Design mobile app screens with Sleek, edit Sleek projects, and implement their designs in React Native or HTML.

Mobile1.1M installs

ios-design

uizze.sh

Design and review iOS UI/UX for iPhone apps in SwiftUI, UIKit, or React Native. Use for mobile app design, screen redesigns, navigation flows, typography, forms, keyboard and safe-area layout, and accessibility checks. Apply Apple Human Interface Guidelines and consult real product references through the optional Uizze connection.

Mobile416.5K installs

vercel-react-native-skills

vercel-labs/agent-skills

React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.

OfficialMobile256.7K installs

animate-expo

emilkowalski/skills

Build animations in React Native and Expo, making the decisions in the order that determines whether they feel right — should it animate, which thread it runs on, which properties, spring or timing, how the gesture hands off, how it degrades. Writes the implementation with Reanimated, Gesture Handler, Expo Router and expo-haptics. Use when animating anything in an Expo app, adding gestures, sheets, screen transitions, press feedback or haptics, or fixing motion that stutters on device. For web animation use animate.

Mobile140.1K installs

xcode-project-setup

firebase/agent-skills

Safely modifies Xcode projects (.pbxproj) to add Swift Packages and link files. Use this skill whenever an iOS project needs dependencies installed (e.g. Firebase, Alamofire).

OfficialMobile131.2K installs

firebase-remote-config-basics

firebase/agent-skills

Manages Firebase Remote Config templates, feature flags, loading strategies, and SDKs (Android, iOS). Use when downloading/deploying remoteconfig JSON templates, managing version history/feature flags, setting in-app defaults, fetchAndActivate(), real-time listeners, or SDK setup. Don't use for Firebase Hosting, Auth, Firestore, Data Connect, Crashlytics, or App Hosting.

OfficialMobile121.7K installs