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:
- User authenticates with their Bitcoin identity via OAuth
- User connects wallet addresses using bitcoin-auth signatures
- Your app requests NFT ownership verification via API
- Sigma Auth queries the blockchain (with pagination) for all NFTs
- 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
nftsarray for preview
Collection Metadata
Collections are identified by the collectionId field in the NFT's metadata:
origin.data.map.subTypeData.collectionIdThis allows flexible collection definitions without relying on origin prefixes.
Next Steps
- Threshold Verification - Token balance checks
- API Reference - Complete API docs
- Examples - Integration examples