Reserve-Claim Pattern: Onboarding Non-Blockchain Users
Overview
Integra’s Reserve-Claim Pattern solves a fundamental blockchain adoption problem: How do you tokenize agreements with people who don’t have crypto wallets?
This pattern enables token issuance for counterparties whose wallet addresses aren’t known at contract creation time, making blockchain accessible to mainstream users who’ve never touched crypto.
The Problem
Traditional blockchain token issuance:
Problem: Need recipient's wallet address BEFORE minting
Blocker: Most people don't have wallets
Result: Can't tokenize real-world agreements with normal peopleReal-world scenario:
- You sell a house to someone who’s never used crypto
- You want the deed as an NFT
- Buyer doesn’t have a wallet yet
- Traditional approach: BLOCKED (need address to mint)
Integra’s Solution
The Reserve-Claim pattern:
1. RESERVE token (address unknown or known)
2. Create CLAIM ATTESTATION (secure permission)
3. Buyer creates wallet (whenever they're ready)
4. CLAIM token using attestation
5. Token MINTED to their new wallet
Result: Blockchain adoption without requiring upfront crypto knowledge.
How It Works
The Document as Identity Anchor
The key innovation: The blockchain-registered document serves as the identity anchor for all parties, even before they have wallets.
// Step 1: Register the real-world contract (property sale)
bytes32 integraHash = documentRegistry.registerDocument(
propertySaleDeedHash, // The real document
ipfsCID,
ownershipTokenizer,
...
);This creates:
- ✅ Permanent document identity (integraHash)
- ✅ Proof of the agreement (documentHash)
- ✅ Owner on record (seller)
- ✅ Identity anchor for future token claims
Three Reservation Methods
Method 1: Named Reservation (Address Known)
When to use: Counterparty has a wallet already
// Reserve for specific address
ownershipTokenizer.reserveToken(
integraHash, // Which document
0, // tokenId (auto-assigned)
buyerWalletAddress, // Known address
1, // amount
processHash
);Method 2: Anonymous Reservation (Address Unknown)
When to use: Counterparty doesn’t have wallet yet
// Reserve for unknown address
ownershipTokenizer.reserveTokenAnonymous(
integraHash, // Which document
0, // tokenId (auto-assigned)
1, // amount
encryptedLabel, // "New Owner" encrypted
processHash
);The encryptedLabel:
// Off-chain: Encrypt role label
const label = "Property Buyer - John Smith";
const key = keccak256(buyerEmailAddress); // Or other secret
const encryptedLabel = encrypt(label, key);
// On-chain: Store encrypted
// Later: Buyer can decrypt to see their roleMethod 3: Approval-Based Reservation
When to use: Pre-approve multiple potential recipients
// Create claim attestation without specific reservation
// Anyone with valid attestation can claim
bytes32 attestationUID = createClaimAttestation(
integraHash,
tokenId,
anyEligibleAddress // Broader approval
);The Complete Flow
Scenario: House Sale with Non-Crypto Buyer
Setup:
- Seller (Alice): Has crypto wallet, owns property
- Buyer (Bob): Never used crypto, wants to buy house
- Problem: Bob has no wallet address
Step 1: Seller Registers Deed
// Alice registers property deed on blockchain
bytes32 deedHash = documentRegistry.registerDocument(
keccak256(physicalDeedPDF),
ipfsHashOfDeed,
ownershipTokenizer,
executor,
processHash,
bytes32(0),
contactResolverId,
[]
);Step 2: Seller Reserves Token (Anonymous)
// Alice reserves deed NFT for "the buyer" (address unknown)
ownershipTokenizer.reserveTokenAnonymous(
deedHash,
0, // Auto-assign tokenId
1,
encryptedLabel, // encrypt("Property Buyer - Bob", sharedSecret)
processHash
);
// Returns: tokenId = 1 (auto-assigned)What’s on-chain now:
Document: deedHash
├─ Owner: Alice (0xAAA...)
├─ Tokenizer: OwnershipTokenizerV7
└─ Reserved Token #1:
├─ For: address(0) (unknown)
├─ Label: 0x encrypted... (buyer can decrypt)
└─ Status: Reserved (not claimed)
Step 3: Seller Creates Claim Attestation
// Alice creates attestation for Bob's email
// (Bob will prove ownership of email to claim)
bytes32 attestationUID = eas.attest({
schema: claimSchemaUID,
data: {
recipient: deriveAddressFromEmail(bobEmail), // Deterministic
data: abi.encode(deedHash, tokenId, CLAIM_CAPABILITY),
expirationTime: 0,
revocable: true
}
});Step 4: Bob Creates Wallet (Weeks Later)
// Bob downloads MetaMask
// Creates wallet: 0xBBB...
// Proves ownership of email via off-chain system
// System generates claim signatureStep 5: Bob Claims Token
// Bob claims the deed NFT
ownershipTokenizer.claimToken(
deedHash,
tokenId,
attestationUID, // Proves he's the buyer
processHash
);What happens:
- ✅ Validates attestation (Bob = authorized recipient)
- ✅ Validates token reserved for this document
- ✅ Mints ERC-721 NFT to Bob’s wallet (0xBBB…)
- ✅ Bob now owns property deed as NFT
- ✅ Trust credentials issued to both Alice and Bob
Final state:
Token #1 (ERC-721):
├─ Owner: Bob (0xBBB...)
├─ Bound to: deedHash
└─ Represents: 123 Main Street property
Alice: Gets trust credential (completed sale as seller)
Bob: Gets trust credential (completed sale as buyer)The Power of Document-Anchored Identity
Why This Works
Traditional NFT approach:
Mint(recipientAddress, tokenId)
↓
Requires: recipientAddress MUST exist before minting
Blocker: Can't tokenize if recipient has no walletIntegra’s document-anchored approach:
Register Document (creates identity anchor)
↓
Reserve Token (for document, not just address)
↓
Create Attestation (proves recipient rights)
↓
Recipient creates wallet (whenever ready)
↓
Claim Token (using attestation)
↓
Minting happens at claim timeThe document (integraHash) anchors the agreement, independent of wallet existence.
Anonymous Reservation Deep Dive
The anonymous reservation pattern solves multiple real-world challenges:
Challenge 1: Recipient Unknown
// Property sale: Know buyer will claim, don't know wallet
reserveTokenAnonymous(deedHash, 0, 1, encrypt("Buyer"), processHash);Challenge 2: Privacy
// Don't want to reveal recipient on-chain yet
reserveTokenAnonymous(
integraHash,
tokenId,
amount,
encrypt("Board Member A"), // Role, not identity
processHash
);Challenge 3: Delegation
// Executor will determine recipient later
reserveTokenAnonymous(
integraHash,
tokenId,
amount,
encrypt("Winner of auction #123"),
processHash
);Real-World Examples
Example 1: Real Estate Transaction
Scenario: Traditional house sale where buyer uses title company, never touched crypto.
// Title company flow
class TitleCompanyIntegration {
async closePropertySale(saleData) {
const { sellerWallet, buyerEmail, propertyDeed } = saleData;
// 1. Register deed (seller has wallet)
const integraHash = await this.registerDeed(
propertyDeed,
sellerWallet
);
// 2. Reserve for buyer (NO WALLET YET)
const label = `Property Buyer - ${buyerEmail}`;
const encryptedLabel = this.encryptLabel(label, buyerEmail);
const tokenId = await this.reserveTokenAnonymous(
integraHash,
encryptedLabel
);
// 3. Create claim attestation tied to email
const attestationUID = await this.createEmailAttestation(
integraHash,
tokenId,
buyerEmail
);
// 4. Send email to buyer
await this.sendEmail(buyerEmail, {
subject: "Your property deed is ready to claim",
body: `
Your property deed for ${propertyDeed.address} is registered on the blockchain.
To claim your digital deed:
1. Create a wallet at https://metamask.io
2. Click this link: ${claimURL}
3. Prove ownership of this email
4. Claim your deed NFT
You can do this anytime - no rush!
`,
claimURL: `https://integra.io/claim/${integraHash}/${tokenId}`,
attestationUID
});
return { integraHash, tokenId, attestationUID };
}
}Months later, buyer creates wallet and claims:
// Buyer clicks claim link, creates MetaMask wallet
// Proves email ownership via OAuth
// System generates claim transaction
await ownershipTokenizer.claimToken(
integraHash,
tokenId,
attestationUID, // Linked to proven email
processHash
);
// Buyer now has property deed NFT in their brand new walletExample 2: Employment Stock Options
Scenario: Company grants stock options to employees without crypto wallets.
// Company grants options to 100 employees
class StockOptionPlan {
async grantOptions(employees) {
const companySharesHash = await this.registerShareDocument();
for (const employee of employees) {
// Reserve shares (employees don't have wallets)
const encryptedLabel = this.encryptLabel(
`Employee - ${employee.name} - ${employee.shares} shares`,
employee.email
);
const tokenId = await sharesTokenizer.reserveTokenAnonymous(
companySharesHash,
0, // Auto-assign
employee.shares, // Amount
encryptedLabel,
processHash
);
// Create claim attestation
const attestationUID = await this.createEmployeeAttestation(
companySharesHash,
tokenId,
employee.email,
employee.hireDate + VESTING_PERIOD
);
// Store for employee
await db.stockGrants.create({
employeeId: employee.id,
integraHash: companySharesHash,
tokenId,
shares: employee.shares,
attestationUID,
vestingDate: employee.hireDate + VESTING_PERIOD,
claimed: false
});
// Email notification
await this.emailEmployee(employee, {
shares: employee.shares,
vestingDate: vestingDate,
claimURL: `https://company.com/claim-shares/${tokenId}`
});
}
}
}Employee claims after vesting:
// 1 year later, employee creates wallet
// Proves email ownership
// Claims shares
await sharesTokenizer.claimToken(
companySharesHash,
tokenId,
attestationUID,
processHash
);
// Employee now has ERC-20 company shares in walletExample 3: Multi-Party Business Agreement
Scenario: 5-person partnership where some partners are crypto-native, others aren’t.
// Partnership formation
const partners = [
{ name: "Alice", wallet: "0xAAA...", role: "CEO" },
{ name: "Bob", wallet: null, role: "CTO" }, // No wallet
{ name: "Carol", wallet: "0xCCC...", role: "CFO" },
{ name: "Dave", wallet: null, role: "COO" }, // No wallet
{ name: "Eve", wallet: null, role: "CMO" } // No wallet
];
// Register partnership agreement
const agreementHash = await documentRegistry.registerDocument(
partnershipAgreementHash,
ipfsCID,
multiPartyTokenizer,
...
);
// Reserve tokens for all partners
for (let i = 0; i < partners.length; i++) {
const partner = partners[i];
const tokenId = i + 1;
if (partner.wallet) {
// Has wallet: Named reservation
await multiPartyTokenizer.reserveToken(
agreementHash,
tokenId,
partner.wallet,
1,
processHash
);
} else {
// No wallet: Anonymous reservation
const label = `${partner.role} - ${partner.name}`;
const encryptedLabel = encrypt(label, partner.email);
await multiPartyTokenizer.reserveTokenAnonymous(
agreementHash,
tokenId,
1,
encryptedLabel,
processHash
);
}
// Create claim attestation for each
await createClaimAttestation(agreementHash, tokenId, partner);
}What happens:
- Alice & Carol: Can claim immediately (have wallets)
- Bob, Dave, Eve: Claim later when they create wallets
- Agreement valid on-chain regardless
- All parties eventually get tokens and trust credentials
The Approval Step: Attestation-Based Claims
Why Attestations?
The reserve-claim pattern uses TokenClaimResolverV7 to ensure security:
Without attestations (insecure):
// Anyone could claim
function claimToken(bytes32 integraHash, uint256 tokenId) external {
_mint(msg.sender, tokenId); // ❌ No authorization!
}With attestations (secure):
// Only authorized recipient can claim
function claimToken(
bytes32 integraHash,
uint256 tokenId,
bytes32 attestationUID // ← Proof of authorization
) external {
// Validates:
// 1. Attestation exists and is valid
// 2. Attestation was created by document owner/executor
// 3. Attestation recipient is msg.sender
// 4. Attestation grants CLAIM capability for this document
_mint(msg.sender, tokenId); // ✅ Authorized!
}Creating Claim Attestations
As document owner:
// Option 1: Specific recipient (known wallet)
bytes32 attestationUID = eas.attest({
schema: claimSchemaUID,
data: {
recipient: buyerWallet, // Specific address
data: abi.encode(integraHash, tokenId, CLAIM_CAPABILITY)
}
});
// Option 2: Derived address (email, phone, SSN, etc.)
bytes32 deterministicAddress = keccak256(abi.encode(
"buyer-email@example.com",
block.chainid
));
bytes32 attestationUID = eas.attest({
schema: claimSchemaUID,
data: {
recipient: deterministicAddress, // Derived from email
data: abi.encode(integraHash, tokenId, CLAIM_CAPABILITY)
}
});Integration Patterns
Pattern 1: Email-Based Claims
Flow:
1. Reserve token anonymously
2. Create attestation for deterministic address derived from email
3. Email user with claim link
4. User proves email ownership (OAuth, magic link, etc.)
5. Backend generates claim transaction with correct wallet
6. User signs and claimsImplementation:
// Backend service
class EmailClaimService {
// Step 1: Reserve and notify
async reserveForEmail(integraHash, recipientEmail) {
// Derive deterministic address from email
const deterministicAddr = keccak256(
abiEncode(["string", "uint256"], [recipientEmail, chainId])
);
// Reserve token
const encryptedLabel = encrypt(
`Buyer - ${recipientEmail}`,
keccak256(recipientEmail)
);
const tx = await tokenizer.reserveTokenAnonymous(
integraHash,
0,
1,
encryptedLabel,
processHash
);
const tokenId = await this.getTokenIdFromEvent(tx);
// Create attestation
const attestationUID = await eas.attest({
schema: claimSchemaUID,
data: {
recipient: deterministicAddr,
data: abi.encode(integraHash, tokenId, CLAIM_CAPABILITY)
}
});
// Store claim info
await db.pendingClaims.create({
email: recipientEmail,
integraHash,
tokenId,
attestationUID,
deterministicAddress: deterministicAddr
});
// Email user
await this.sendClaimEmail(recipientEmail, integraHash, tokenId);
}
// Step 2: User proves email and claims
async processEmailClaim(email, userWallet, emailProof) {
// Verify email ownership
await this.verifyEmailProof(email, emailProof);
// Get pending claim
const claim = await db.pendingClaims.findOne({ email });
// Execute claim (user signs with their wallet)
const tx = await tokenizer.claimToken(
claim.integraHash,
claim.tokenId,
claim.attestationUID,
processHash
);
// Mark as claimed
await db.pendingClaims.update(
{ email },
{ claimed: true, claimedBy: userWallet, claimedAt: Date.now() }
);
return tx;
}
}Pattern 2: SMS-Based Claims
For mobile-first users:
// Reserve for phone number
const phoneNumber = "+1-555-0123";
const deterministicAddr = keccak256(
abiEncode(["string"], [phoneNumber])
);
await tokenizer.reserveTokenAnonymous(integraHash, 0, 1, encrypted, processHash);
await createAttestation(integraHash, tokenId, deterministicAddr);
// SMS to user
await sendSMS(phoneNumber, `
You have a blockchain asset waiting!
Click: ${claimURL}
No crypto knowledge needed - we'll guide you.
`);Pattern 3: QR Code Claims
For in-person onboarding:
// Event: Conference giving NFT badges to attendees
// 1. Pre-register badges
const badgeHash = await registerBadgeDocument();
// 2. Reserve 1000 anonymous badges
for (let i = 0; i < 1000; i++) {
await badgeTokenizer.reserveTokenAnonymous(
badgeHash,
i,
1,
encrypt(`Attendee ${i}`),
processHash
);
}
// 3. Generate unique QR codes
for (let i = 0; i < 1000; i++) {
const claimSecret = generateSecret();
const attestationUID = await createAttestation(
badgeHash,
i,
deriveAddress(claimSecret)
);
const qrCode = generateQRCode({
url: `https://event.com/claim/${i}`,
secret: claimSecret,
attestation: attestationUID
});
printBadge(i, qrCode);
}
// 4. Attendee scans QR
// 5. Creates wallet or connects existing
// 6. Claims badge NFTEncrypted Labels: Privacy-Preserving Role Identification
What are Encrypted Labels?
When reserving anonymously, you can attach an encrypted label that describes the recipient’s role:
// Plain text
const label = "Property Buyer - Lot 5 - John Smith";
// Encrypt with shared secret
const key = keccak256(buyerEmail); // Deterministic key
const encryptedLabel = encrypt(label, key);
// Store on-chain
await tokenizer.reserveTokenAnonymous(
integraHash,
tokenId,
amount,
encryptedLabel, // Encrypted blob
processHash
);Benefits:
- ✅ On-chain: Encrypted (no one can read)
- ✅ Off-chain: Buyer can decrypt with key
- ✅ Identifies role without revealing identity
- ✅ Useful for UX (show user their pending claims)
Decryption Flow
// Buyer retrieves encrypted labels for their email
const pendingReservations = await tokenizer.queryFilter(
tokenizer.filters.TokenReservedAnonymous(integraHash)
);
// Try decrypting each
for (const reservation of pendingReservations) {
const key = keccak256(myEmail);
try {
const label = decrypt(reservation.encryptedLabel, key);
console.log(`You have a pending claim: ${label}`);
// Shows: "Property Buyer - Lot 5 - John Smith"
} catch {
// Not for this user
}
}Comparison with Traditional Approaches
Approach 1: Require Wallet Upfront
Traditional dApp:
"Connect wallet to continue"
↓
User leaves if they don't have wallet
↓
Adoption failure: 95% drop-offApproach 2: Custodial Wallets
Centralized exchange approach:
"We'll create a wallet for you"
↓
Custodial (exchange controls keys)
↓
Not self-custody, defeats blockchain purposeApproach 3: Integra Reserve-Claim
Integra approach:
"We'll reserve your token, claim when ready"
↓
User creates wallet at their own pace
↓
Self-custody, full blockchain benefits
↓
Adoption success: Low friction onboardingSecurity Model
Authorization Flow
Reserve (Owner/Executor required):
├─ Validates: msg.sender is document owner OR executor
├─ Creates: Reservation record
└─ Emits: TokenReserved or TokenReservedAnonymous
Attestation Creation (Owner/Executor required):
├─ Validates: Via TokenClaimResolver
├─ Checks: Creator is authorized for document
├─ Creates: EAS attestation
└─ Binds: Attestation to specific recipient
Claim (Recipient required):
├─ Validates: Attestation exists and valid
├─ Checks: msg.sender is attestation recipient
├─ Verifies: Attestation grants CLAIM capability
├─ Mints: Token to msg.sender
└─ Emits: TokenClaimed eventAttack Prevention
Attack 1: Unauthorized Claim
Attacker tries: claimToken(integraHash, tokenId, fakeAttestation)
Prevention: TokenClaimResolver validates attestation
Result: ❌ Claim reverted (invalid attestation)Attack 2: Malicious Reservation
Attacker tries: reserveToken(victimDocument, tokenId, attackerAddr)
Prevention: requireOwnerOrExecutor modifier
Result: ❌ Transaction reverted (not authorized)Attack 3: Attestation Theft
Attacker tries: Use someone else's attestation UID
Prevention: Attestation specifies recipient address
Result: ❌ Claim reverted (wrong recipient)Integration Benefits
For Traditional Businesses
- No Blockchain Expertise Required from end users
- Gradual Adoption - users join when ready
- Familiar UX - email/SMS based claims
- Self-Custody - users eventually control keys
- Compliance Ready - audit trail via processHash
For Developers
- Simple API - reserve + attest + claim
- Flexible - named or anonymous reservations
- Secure - attestation-based authorization
- Integrated - processHash links to your workflows
- Standard - ERC tokens work everywhere after claim
For Users
- No Upfront Wallet - create when ready
- Guided Onboarding - step-by-step claim process
- Email Notifications - familiar communication
- True Ownership - self-custody after claim
- Portable - standard tokens work everywhere
Summary
Integra’s Reserve-Claim Pattern:
- Enables token issuance for users without wallets
- Uses blockchain-registered document as identity anchor
- Supports both named (address known) and anonymous (address unknown) reservations
- Secures claims via attestation-based authorization (TokenClaimResolver)
- Preserves privacy via encrypted labels
- Integrates with traditional systems via processHash correlation
- Provides gradual, low-friction blockchain onboarding
This makes Integra the bridge between traditional business processes and blockchain technology, enabling mainstream adoption without requiring upfront crypto expertise.
Learn More
- TokenClaimResolverV7 - Attestation validation
- Document-Token Binding - Document identity architecture
- ProcessHash Integration - Workflow correlation
- Trust Graph - Reputation building
- Integration Guide - Complete integration examples