Sigma Auth
Access Control

Token Threshold Verification

Token Threshold Verification allows you to create tiered access levels based on how many NFTs users hold from a collection, enabling loyalty programs, governance systems, and VIP tiers.

Threshold verification works with the same /api/wallet/verify-ownership endpoint as NFT verification, using the minCount parameter.

How It Works

The threshold verification system:

  1. User authenticates with their Bitcoin identity
  2. User connects wallet addresses (using bitcoin-auth)
  3. Your app defines threshold tiers (e.g., Bronze: 10, Gold: 50, Platinum: 100)
  4. Sigma Auth checks collection ownership with pagination
  5. Returns whether user meets the threshold, along with actual count

Use Cases

Tiered VIP Programs

Create multiple access tiers based on NFT holdings:

const tiers = {
  bronze: 10,    // 10+ NFTs
  silver: 50,    // 50+ NFTs
  gold: 100,     // 100+ NFTs
  platinum: 250  // 250+ NFTs
};

async function getUserTier(sessionToken, collectionId) {
  // Check user's total NFT count
  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 { count } = await response.json();

  // Determine tier
  if (count >= tiers.platinum) return 'platinum';
  if (count >= tiers.gold) return 'gold';
  if (count >= tiers.silver) return 'silver';
  if (count >= tiers.bronze) return 'bronze';
  return 'free';
}

Governance Voting

Weight voting power by NFT holdings:

// Check if user has minimum NFTs to vote
const minVotingNFTs = 1;
const maxVotingPower = 100; // Cap voting power

async function getVotingPower(sessionToken, collectionId) {
  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,
      minCount: minVotingNFTs
    })
  });

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

  if (!owns) return 0; // Below minimum threshold

  // Weight by holdings but cap at max
  return Math.min(count, maxVotingPower);
}

Loyalty Rewards

Unlock benefits based on collection size:

const rewards = {
  member: 5,      // 5+ NFTs: 5% discount
  vip: 20,        // 20+ NFTs: 10% discount + free shipping
  elite: 100      // 100+ NFTs: 20% discount + early access
};

async function getDiscount(sessionToken, collectionId) {
  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 { count } = await response.json();

  if (count >= rewards.elite) return { tier: 'elite', discount: 0.20, freeShipping: true, earlyAccess: true };
  if (count >= rewards.vip) return { tier: 'vip', discount: 0.10, freeShipping: true };
  if (count >= rewards.member) return { tier: 'member', discount: 0.05 };
  return { tier: 'free', discount: 0 };
}

API Usage

Basic Threshold Check

// Example: Check if user owns at least 50 NFTs from a collection
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: '1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0', // Collection ID
    minCount: 50 // Threshold
  })
});

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

if (owns) {
  console.log(`Threshold met! User has ${count} NFTs (>= 50)`);
} else {
  console.log(`Threshold not met. User has ${count} NFTs (< 50)`);
}

Response Format

// User has 268 NFTs, threshold is 50
{
  "owns": true,   // Meets threshold (268 >= 50)
  "count": 268,   // Total NFTs from collection
  "nfts": [ /* ... first 100 NFTs ... */ ]
}

// User has 25 NFTs, threshold is 50
{
  "owns": false,  // Below threshold (25 < 50)
  "count": 25     // Total NFTs from collection
}

// User has exactly 100 NFTs, threshold is 100
{
  "owns": true,   // Meets threshold (100 >= 100)
  "count": 100
}

Multi-Tier Verification

Check against multiple thresholds efficiently:

const collectionId = '1611d956f397caa80b56bc148b4bce87b54f39b234aeca4668b4d5a7785eb9fa_0';
const thresholds = [10, 50, 100, 250];

async function checkAllTiers(sessionToken) {
  // Single API call to get total count
  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 { count } = await response.json();

  // Check all thresholds locally
  const results = thresholds.map(threshold => ({
    threshold,
    meets: count >= threshold
  }));

  return { count, results };
}

// Example output:
// {
//   count: 75,
//   results: [
//     { threshold: 10, meets: true },
//     { threshold: 50, meets: true },
//     { threshold: 100, meets: false },
//     { threshold: 250, meets: false }
//   ]
// }

Dynamic Tier Calculation

Calculate the highest tier a user qualifies for:

function calculateTier(count, thresholds) {
  // Sort thresholds descending
  const sorted = Object.entries(thresholds).sort((a, b) => b[1] - a[1]);

  // Find highest tier user qualifies for
  for (const [tierName, minCount] of sorted) {
    if (count >= minCount) {
      return tierName;
    }
  }

  return null; // No tier
}

// Example
const count = 75;
const thresholds = {
  bronze: 10,
  silver: 50,
  gold: 100,
  platinum: 250
};

const tier = calculateTier(count, thresholds);
// Returns: "silver" (highest tier where 75 >= threshold)

Best Practices

Cache Aggressively

NFT counts change infrequently. Cache longer than basic ownership checks:

// Cache for 15 minutes (NFTs don't transfer that often)
const cacheKey = `nft-count-${userId}-${collectionId}`;
let result = cache.get(cacheKey);

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

Progressive Disclosure

Show users their current tier and next milestone:

async function getTierProgress(sessionToken, collectionId, thresholds) {
  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 { count } = await response.json();

  // Find current tier
  const tiers = Object.entries(thresholds).sort((a, b) => a[1] - b[1]);
  let currentTier = null;
  let currentIndex = -1;

  for (let i = tiers.length - 1; i >= 0; i--) {
    if (count >= tiers[i][1]) {
      currentTier = tiers[i][0];
      currentIndex = i;
      break;
    }
  }

  // Find next tier
  let nextTier = null;
  if (currentIndex < tiers.length - 1) {
    const [name, threshold] = tiers[currentIndex + 1];
    nextTier = {
      name,
      threshold,
      needed: threshold - count
    };
  }

  return {
    count,
    currentTier,
    nextTier
  };
}

// Example usage
const progress = await getTierProgress(sessionToken, collectionId, {
  bronze: 10,
  silver: 50,
  gold: 100
});

console.log(`You have ${progress.count} NFTs`);
console.log(`Current tier: ${progress.currentTier || 'None'}`);
if (progress.nextTier) {
  console.log(`${progress.nextTier.needed} more NFTs to reach ${progress.nextTier.name}`);
}

Threshold Optimization

Choose thresholds that create meaningful tiers:

// ❌ Bad: Tiers too close together
const badTiers = {
  bronze: 10,
  silver: 15,  // Only 5 NFTs difference
  gold: 20
};

// ✅ Good: Clear progression
const goodTiers = {
  bronze: 10,
  silver: 50,   // 5x increase
  gold: 100,    // 2x increase
  platinum: 250 // 2.5x increase
};

Handle Edge Cases

Always account for zero holdings and exact matches:

async function checkThreshold(sessionToken, collectionId, minCount) {
  try {
    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, minCount })
    });

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

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

    return {
      meetsThreshold: owns,
      actualCount: count,
      shortage: owns ? 0 : minCount - count
    };
  } catch (error) {
    console.error('Threshold check failed:', error);
    return { meetsThreshold: false, actualCount: 0, shortage: minCount };
  }
}

Technical Details

How Thresholds Work

The API uses a simple >= comparison:

const owns = matchingNFTs.length >= minCount;

This means:

  • count: 100, minCount: 100owns: true ✅ (exact match passes)
  • count: 100, minCount: 99owns: true
  • count: 100, minCount: 101owns: false

Pagination Impact

The API paginates through ALL NFTs before checking the threshold:

  • Fetches 100 NFTs per page
  • Continues until no more results
  • Counts total across all pages
  • Then compares against threshold

This ensures accurate threshold checks even for large collections.

Default Threshold

If you omit minCount, it defaults to 1:

// These are equivalent:
{ origin: collectionId }
{ origin: collectionId, minCount: 1 }

Supported Collections

BSV 1Sat Ordinals

  • Full pagination support
  • Real-time blockchain verification
  • Collection metadata-based identification

Future Support (Planned)

  • ERC-721/ERC-1155 (Ethereum, Polygon, Base)
  • SPL tokens (Solana)
  • Other NFT standards

Performance Considerations

API Call Optimization

Fetch once, check multiple thresholds locally:

// ✅ Good: Single API call
const { count } = await verifyOwnership({ origin: collectionId });
const bronze = count >= 10;
const silver = count >= 50;
const gold = count >= 100;

// ❌ Bad: Multiple API calls
const bronze = await verifyOwnership({ origin: collectionId, minCount: 10 });
const silver = await verifyOwnership({ origin: collectionId, minCount: 50 });
const gold = await verifyOwnership({ origin: collectionId, minCount: 100 });

Caching Strategy

// Cache the count, not the threshold result
const cacheKey = `nft-count-${userId}-${collectionId}`;
let count = cache.get(cacheKey);

if (count === undefined) {
  const response = await verifyOwnership({ origin: collectionId });
  count = response.count;
  cache.set(cacheKey, count, 900); // 15 minutes
}

// Check thresholds from cached count
const meetsSilver = count >= 50;
const meetsGold = count >= 100;

Next Steps