> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solanamobile.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Seeker Genesis Token

> The Seeker Genesis Token is a unique NFT that represents a verified owner of a Seeker device.

export const FooterDisclaimer = () => {
  return <p className="not-prose mt-16 text-center text-xs text-gray-500 dark:text-gray-400">
      Code samples on this page are subject to the{" "}
      <a className="underline underline-offset-2" href="https://www.apache.org/licenses/LICENSE-2.0">
        Apache 2.0 license
      </a>
      .
    </p>;
};

The Seeker Genesis Token is a unique NFT that represents a verified owner of a [Seeker](/solana-mobile-stack/seeker) device. It can only be minted once per device and is minted into the primary account in a user's [Seed Vault Wallet](/solana-mobile-stack/seed-vault).

## NFT Details

The Seeker Genesis Token implements Token Extensions (formerly Token-2022).

### Transferability

The Seeker Genesis Token can only be transferred between a user's wallet accounts on a permissioned basis within the Seed Vault Wallet.
A transfer occurs when a user changes their primary account in the Seed Vault Wallet.

The **mint address** of the SGT remains the same when it is transferred.

#### Anti-Sybil Example: In-App Rewards Claim

Even with transferability, the Seeker Genesis Token is still useful as an anti-sybil measure.

As an example, imagine an in-app rewards claim that wants to use the Seeker Genesis Token to limit the claim to once per Seeker device.

To implement this, the app should check for 3 properties:

1. The connected wallet owns a Seeker Genesis Token.
2. The user proves ownership of the wallet via [signing a message](https://github.com/phantom/sign-in-with-solana?tab=readme-ov-file#dapp-integration).
3. The Seeker Genesis Token has not been previously used to claim a reward (i.e check that its **mint address** has not been seen before).

Step 3 is crucial to exclude wallets that have previously claimed the reward on another wallet (and then transferred the SGT to a new wallet).

### Token Extensions

Seeker Genesis Token implements several extensions, notably:

* Metadata Pointer
* Token Group Member and Pointer

### Key Addresses

* **Metadata Address**: `GT22s89nU4iWFkNXj1Bw6uYhJJWDRPpShHt4Bk8f99Te`
* **Group Address**: `GT22s89nU4iWFkNXj1Bw6uYhJJWDRPpShHt4Bk8f99Te`

#### View on an Explorer

* An individual [Seeker Genesis Token](https://explorer.solana.com/address/5mXbkqKz883aufhAsx3p5Z1NcvD2ppZbdTTznM6oUKLj/token-extensions) and its extensions
* Seeker Genesis Token [Metadata Account](https://explorer.solana.com/address/GT22s89nU4iWFkNXj1Bw6uYhJJWDRPpShHt4Bk8f99Te/)

## Verifying Seeker Genesis Token Ownership

To verify a wallet owns a Seeker Genesis Token, you can use the [`getTokenAccountsByOwnerV2` API call](https://www.helius.dev/docs/api-reference/rpc/http/gettokenaccountsbyownerv2) provided by Helius.

Two details matter for a correct check:

* **Skip empty token accounts.** Transferring an SGT out of a wallet leaves the old token account open with a balance of `0`, so a wallet that once held an SGT will keep returning that account. Only a token account with a non-zero balance counts as current ownership.
* **Return the mint address, not a boolean.** The [Anti-Sybil Example](#anti-sybil-example-in-app-rewards-claim) above requires recording the SGT's mint address, so the check should return it.

The script below uses `@solana/kit` and the Token-2022 client. Install them first:

```shell theme={null}
npm install @solana/kit @solana-program/token-2022
```

Here's an example script that returns the SGT's mint address if the wallet currently holds one, or `null` otherwise:

```js theme={null}
import { createSolanaRpc, unwrapOption } from "@solana/kit";
import { fetchAllMaybeMint } from "@solana-program/token-2022";

async function checkWalletForSGT(walletAddress) {
  const HELIUS_RPC_URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`;

  // The metadata mint and group mint address are intentionally the same.
  const SGT_METADATA_ADDRESS = "GT22s89nU4iWFkNXj1Bw6uYhJJWDRPpShHt4Bk8f99Te";
  const SGT_GROUP_MINT_ADDRESS = "GT22s89nU4iWFkNXj1Bw6uYhJJWDRPpShHt4Bk8f99Te";

  try {
    const rpc = createSolanaRpc(HELIUS_RPC_URL);

    // getTokenAccountsByOwnerV2 is a Helius extension rather than a standard
    // RPC method, so call it over plain JSON-RPC with pagination.
    const allTokenAccounts = [];
    let paginationKey = undefined;
    let pageCount = 0;

    console.log(`Starting paginated fetch for wallet: ${walletAddress}`);

    do {
      pageCount++;
      console.log(`Fetching page ${pageCount}...`);

      const requestPayload = {
        jsonrpc: "2.0",
        id: `page-${pageCount}`,
        method: "getTokenAccountsByOwnerV2",
        params: [
          walletAddress,
          { programId: "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" }, // Token-2022 program
          {
            encoding: "jsonParsed",
            limit: 1000, // Maximum accounts per request
            ...(paginationKey && { paginationKey }),
          },
        ],
      };

      const response = await fetch(HELIUS_RPC_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(requestPayload),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();

      if (data.error) {
        throw new Error(`RPC error: ${data.error.message}`);
      }

      // getTokenAccountsByOwnerV2 shapes its result differently depending on
      // withContext: without it, the accounts are the array at result.value and
      // the key is at result.paginationKey; with it, both sit under result.value.
      const page = Array.isArray(data.result?.value)
        ? {
            accounts: data.result.value,
            paginationKey: data.result.paginationKey,
          }
        : (data.result?.value ?? {});

      const pageResults = page.accounts ?? [];
      console.log(
        `Page ${pageCount}: Found ${pageResults.length} token accounts`,
      );

      allTokenAccounts.push(...pageResults);
      paginationKey = page.paginationKey;
    } while (paginationKey); // Continue until no more pages

    console.log(
      `\nCompleted pagination: ${pageCount} pages, ${allTokenAccounts.length} total token accounts`,
    );

    // Extract mint addresses from token accounts.
    // Skip token accounts with a zero balance: transferring an SGT out of a
    // wallet leaves the old token account open at a balance of 0, and it
    // must not count as current ownership.
    const mintAddresses = allTokenAccounts
      .map((accountInfo) => accountInfo?.account?.data?.parsed?.info)
      .filter((info) => info?.mint && info.tokenAmount?.amount !== "0")
      .map((info) => info.mint);

    console.log(`Extracted ${mintAddresses.length} mint addresses`);

    // Fetch and decode mint accounts in batches of 100 to avoid RPC limits
    const BATCH_SIZE = 100;

    for (let i = 0; i < mintAddresses.length; i += BATCH_SIZE) {
      const batch = mintAddresses.slice(i, i + BATCH_SIZE);
      console.log(
        `Fetching mint info batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(mintAddresses.length / BATCH_SIZE)}`,
      );

      const mints = await fetchAllMaybeMint(rpc, batch);

      for (const mint of mints) {
        // Skip addresses that have no account data on chain
        if (!mint.exists) continue;

        const extensions = unwrapOption(mint.data.extensions) ?? [];

        // Check for correct SGT Metadata
        const metadataPointer = extensions.find(
          (ext) => ext.__kind === "MetadataPointer",
        );
        const hasCorrectMetadata =
          metadataPointer !== undefined &&
          unwrapOption(metadataPointer.metadataAddress) ===
            SGT_METADATA_ADDRESS;

        // Check for correct SGT Group Member
        const groupMember = extensions.find(
          (ext) => ext.__kind === "TokenGroupMember",
        );
        const hasCorrectGroupMember =
          groupMember !== undefined &&
          groupMember.group === SGT_GROUP_MINT_ADDRESS;

        // If both extensions match then it is an SGT
        if (hasCorrectMetadata && hasCorrectGroupMember) {
          console.log(
            `\nVERIFIED SGT FOUND: Wallet holds a verified SGT (${mint.address}).`,
          );
          return mint.address;
        }
      }
    }

    // No verified SGT found in wallet
    console.log("\nNo verified SGT found in wallet.");
    return null;
  } catch (error) {
    console.error("Error verifying SGT ownership:", error.message);
    return null;
  }
}
```

<FooterDisclaimer />
