Sigma Auth
Access Control

NFT Verification

NFT Verification allows you to check if authenticated users own specific NFTs or ordinals from a collection, enabling membership-based access control with threshold support.

How It Works

The NFT verification system:

  1. User authenticates with their Bitcoin identity via OAuth
  2. User connects wallet addresses using bitcoin-auth signatures
  3. Your app requests NFT ownership verification via API
  4. Sigma Auth queries the blockchain (with pagination) for all NFTs
  5. Returns verification result with count and NFT details

Supported Standards

1Sat Ordinals (BSV)

Native support for Bitcoin SV ordinals:

  • Collection-based verification - Check if user owns any NFT from a collection
  • Threshold verification - Require minimum number of NFTs (e.g., "must own 50+ Pixel Foxes")
  • UTXO validation - Real-time blockchain verification
  • Pagination - Fetches all NFTs, not just first 100

Collection Identification

NFTs are identified by their collectionId in the metadata:

{
  "origin": {
    "outpoint": "8328b6d...4987b_37",
    "data": {
      "map": {
        "subTypeData": {
          "collectionId": "1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0"
        }
      }
    }
  }
}

Wallet Connection

Before verification, users must connect their wallet addresses using cryptographic signatures.

Using Yours Wallet (1sat.market)

import { useYoursWallet } from '@/lib/hooks/use-yours-wallet';
import { createHash } from 'crypto';

const wallet = useYoursWallet();

// 1. Connect to Yours Wallet
await wallet.connect();

// 2. Construct bitcoin-auth message
const timestamp = new Date().toISOString();
const requestPath = '/api/wallet/connect';
const bodyHash = createHash('sha256').update('').digest('hex');
const message = `${requestPath}|${timestamp}|${bodyHash}`;

// 3. Sign with ordinals key (important for NFT verification!)
const yoursSignature = await wallet.signMessage({
  message,
  encoding: 'utf8',
  tag: {
    label: 'yours',
    id: 'ord',      // Use 'ord' for ordinals address (holds NFTs)
    domain: '',
    meta: {}
  }
});

// 4. Construct bitcoin-auth token
const authToken = `${yoursSignature.pubKey}|bsm|${timestamp}|${requestPath}|${yoursSignature.sig}`;

// 5. Connect wallet
const response = await fetch('https://auth.sigmaidentity.com/api/wallet/connect', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Cookie': 'better-auth.session_token=...'
  },
  body: JSON.stringify({ authToken })
});

const { walletAddress, pubkey } = await response.json();

Use tag.id='ord' to connect the ordinals address. This is the address that holds your NFTs. Using tag.id='bsv' (payment key) or tag.id='identity' will connect different addresses.

API Usage

Check NFT Ownership

// Example: Check if user owns any Pixel Foxes NFTs
const response = await fetch('https://auth.sigmaidentity.com/api/wallet/verify-ownership', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Cookie': 'better-auth.session_token=...' // Session from OAuth flow
  },
  body: JSON.stringify({
    origin: '1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0', // Collection ID
  })
});

const { owns, count, nfts } = await response.json();

if (owns) {
  console.log(`User owns ${count} NFTs from this collection!`);
  // Grant access to premium content
}

Check with Threshold

// Example: Require minimum 50 NFTs for VIP access
const response = await fetch('https://auth.sigmaidentity.com/api/wallet/verify-ownership', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Cookie': 'better-auth.session_token=...'
  },
  body: JSON.stringify({
    origin: '1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0',
    minCount: 50  // Require at least 50 NFTs
  })
});

const { owns, count } = await response.json();

if (owns) {
  console.log(`User owns ${count} NFTs - VIP access granted!`);
} else {
  console.log(`User owns ${count} NFTs - need ${50 - count} more for VIP`);
}

Response Format

// Success response
{
  "owns": true,
  "count": 268,  // Total NFTs owned from this collection
  "nfts": [      // Array of NFT objects (up to 100 returned, but count is total)
    {
      "txid": "899d447b...",
      "outpoint": "899d447b...2fbf_0",
      "satoshis": 1,
      "owner": "139xRf73Vw3W8cMNoXW9amqZfXMrEuM9XQ",
      "origin": {
        "outpoint": "8328b6d...4987b_37",
        "data": {
          "map": {
            "name": "Pixel Foxes #2031290",
            "subTypeData": {
              "collectionId": "1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0"
            }
          }
        }
      }
    }
    // ... more NFTs
  ]
}

// No NFTs found
{
  "owns": false,
  "count": 0
}

// Not authenticated
{
  "error": "unauthorized",
  "message": "Authentication required"
}

Use Cases

Tiered Membership

Create access tiers based on NFT count:

async function checkMembershipTier(sessionToken) {
  const collectionId = '1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0';

  // Check ownership
  const response = await fetch('https://auth.sigmaidentity.com/api/wallet/verify-ownership', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Cookie': `better-auth.session_token=${sessionToken}`
    },
    body: JSON.stringify({ origin: collectionId })
  });

  const { owns, count } = await response.json();

  if (!owns) return 'free';
  if (count >= 100) return 'platinum';
  if (count >= 50) return 'gold';
  if (count >= 10) return 'silver';
  return 'bronze';
}

Subscription NFTs

Use collection origins as subscription tokens:

// Each month has a different collection
const subscriptionCollections = {
  '2025-01': '1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0',
  '2025-02': '2722e067f408dbb793b27c261722f66ae54062c345bfdb5779b5e6b896fec0gb_0',
};

async function hasActiveSubscription(sessionToken) {
  const currentMonth = '2025-01';
  const collectionId = subscriptionCollections[currentMonth];

  const response = await fetch('https://auth.sigmaidentity.com/api/wallet/verify-ownership', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Cookie': `better-auth.session_token=${sessionToken}`
    },
    body: JSON.stringify({ origin: collectionId })
  });

  const { owns } = await response.json();
  return owns;
}

Whale Detection

Check for large holders:

async function isWhale(sessionToken, threshold = 1000) {
  const response = await fetch('https://auth.sigmaidentity.com/api/wallet/verify-ownership', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Cookie': `better-auth.session_token=${sessionToken}`
    },
    body: JSON.stringify({
      origin: 'collection-id-here',
      minCount: threshold
    })
  });

  const { owns, count } = await response.json();
  return { isWhale: owns, nftCount: count };
}

Best Practices

Caching

Cache verification results to reduce API load:

// Cache verification for 5 minutes
const cacheKey = `nft-verify-${userId}-${collectionId}`;
let result = cache.get(cacheKey);

if (!result) {
  result = await verifyOwnership({ origin: collectionId });
  cache.set(cacheKey, result, 300); // 5 minutes TTL
}

Progressive Disclosure

Show users their progress toward thresholds:

const { owns, count } = await verifyOwnership({ origin: collectionId });

const thresholds = {
  bronze: 1,
  silver: 10,
  gold: 50,
  platinum: 100
};

// Find current tier
let currentTier = 'free';
for (const [tier, min] of Object.entries(thresholds).reverse()) {
  if (count >= min) {
    currentTier = tier;
    break;
  }
}

// Find next tier
const nextTier = Object.entries(thresholds).find(([_, min]) => min > count);
if (nextTier) {
  const [name, needed] = nextTier;
  console.log(`${needed - count} more NFTs to reach ${name}`);
}

Error Handling

Always handle verification failures:

try {
  const response = await fetch('/api/wallet/verify-ownership', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ origin: collectionId })
  });

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

  const { owns, count } = await response.json();
  return { owns, count };
} catch (error) {
  console.error('Verification failed:', error);
  // Fallback: deny access gracefully
  return { owns: false, count: 0 };
}

Technical Details

Pagination

The API automatically paginates through all NFTs (100 per page) to ensure accurate counts:

  • User with 268 NFTs will see count: 268 (not just 100)
  • Threshold checks work across all pages
  • First 100 NFTs returned in nfts array for preview

Collection Metadata

Collections are identified by the collectionId field in the NFT's metadata:

origin.data.map.subTypeData.collectionId

This allows flexible collection definitions without relying on origin prefixes.

Next Steps