Tokenize an Asset
Step-by-step guide to tokenizing an Integra record as an ERC-721, ERC-20, or ERC-1155 token.
This guide walks you through the token lifecycle: choosing a tokenizer, reserving a token position, claiming it with an EAS attestation, and using the resulting token. By the end, your Integra record will be represented as a standard ERC token that can be transferred, queried, and composed with other protocols.
Prerequisites
Before tokenizing, you need:
- An existing record in
IntegraRecordV1-- see Register a Record if you have not created one yet - A tokenizer associated with the record -- either set during registration or via
associateTokenizer() - An EAS attestation -- required for the claim step (issued by the record owner or a delegated issuer)
How Tokenization Works
All 23 Integra tokenizers follow the same lifecycle, regardless of which ERC standard they use:
(none) --> RESERVED --> CLAIMED (minted) --> IN USE
|
v
CANCELLED- Reserve -- The record owner (or authorized executor) allocates a token slot without minting.
- Claim -- The intended recipient presents a valid EAS attestation and the token is minted to them.
- Use -- The token behaves as a standard ERC-20, ERC-721, or ERC-1155 token.
This reserve-then-claim pattern ensures that tokens are only minted to verified participants. An attestation issued for one record cannot be used to claim a token from a different record.
Step 1: Choose a Tokenizer
Integra provides 23 tokenizers across three ERC standards. Here is a simplified decision guide:
ERC-721: One Token Per Record
Best for assets with a single owner -- deeds, titles, certificates.
| Tokenizer | Best For |
|---|---|
| OwnershipTokenizerV1 | General single-owner, transferable assets |
| SoulboundTokenizerV1 | Non-transferable credentials (diplomas, certifications) |
| LicenseTokenizerV1 | Software/professional licenses with expiration |
| EscrowTokenizerV1 | Escrow agreements with conditional release |
| DebtTokenizerV1 | Debt instruments, promissory notes |
| InvoiceTokenizerV1 | Invoices and trade receivables |
ERC-20: Fungible Shares Per Record
Best for fractional ownership or governance -- each token is interchangeable.
| Tokenizer | Best For |
|---|---|
| SharesTokenizerV1 | Company equity, partnership interests |
| GovernanceTokenizerV1 | DAO governance with delegation and checkpoints |
| SecurityTokenTokenizerV1 | Regulated securities with transfer restrictions |
| FractionalTokenizerV1 | Fractional ownership with buyout/voting |
ERC-1155: Multiple Tokens Per Record
Best for multi-party agreements where each participant gets a distinct token.
| Tokenizer | Best For |
|---|---|
| MultiPartyTokenizerV1 | Multi-stakeholder agreements (closings, joint ventures) |
| RoyaltyTokenizerV1 | Royalty splits, revenue sharing |
| BadgeTokenizerV1 | Achievement badges, credentials |
| TrustTokenizerV1 | Trust agreements with multiple beneficiary classes |
| RentalTokenizerV1 | Time-bounded rental or lease access |
For the full list with detailed comparisons, see Choosing a Tokenizer.
Step 2: Associate the Tokenizer
If you did not set a tokenizer during record registration, associate one now. This is a one-time, irreversible operation:
In Solidity
// The tokenizer must be registered and active in IntegraRegistryV1
record.associateTokenizer(integraHash, ownershipTokenizerAddress, processHash);In TypeScript (ethers.js v6)
const tx = await record.associateTokenizer(
integraHash,
OWNERSHIP_TOKENIZER_ADDRESS,
processHash
);
await tx.wait();
console.log("Tokenizer associated");After this step, the tokenizer contract is permanently bound to the record. The tokenizer verifies this binding on every operation -- if a different tokenizer tries to operate on this record, the call reverts with WrongTokenizer.
Step 3: Reserve a Token Position
Reservation allocates a token slot without minting. There are two reservation modes:
Named Reservation (Recipient Known)
Use this when you know who will receive the token:
// ERC-721 tokenizer: one token per record, tokenId is typically 1
tokenizer.reserveToken(
integraHash, // record identifier
1, // tokenId
recipientAddress, // who will claim this token
1, // amount (always 1 for ERC-721)
processHash // workflow correlation ID
);// ethers.js v6
const tx = await tokenizer.reserveToken(
integraHash,
1n, // tokenId
recipientAddress,
1n, // amount
processHash
);
await tx.wait();Anonymous Reservation (Recipient Unknown)
Use this when the recipient is not yet known. An encrypted label identifies the role:
tokenizer.reserveTokenAnonymous(
integraHash, // record identifier
1, // tokenId
1, // amount
encryptedLabel, // up to 500 bytes identifying the role (e.g., encrypted "Buyer")
processHash // workflow correlation ID
);// ethers.js v6
const label = ethers.toUtf8Bytes("Buyer"); // In production, encrypt this
const tx = await tokenizer.reserveTokenAnonymous(
integraHash,
1n,
1n,
label,
processHash
);
await tx.wait();ERC-20 Example (Fungible Shares)
For ERC-20 tokenizers, the amount parameter controls how many tokens to reserve:
// Reserve 1000 shares for a known recipient
const tx = await sharesTokenizer.reserveToken(
integraHash,
0n, // tokenId is 0 for ERC-20 tokenizers
recipientAddress,
ethers.parseUnits("1000", 18), // 1000 tokens
processHash
);ERC-1155 Example (Multi-Party)
For ERC-1155 tokenizers, each tokenId represents a different role or party:
// Reserve token for Buyer (tokenId = 1)
await multiPartyTokenizer.reserveToken(
integraHash, 1n, buyerAddress, 1n, processHash
);
// Reserve token for Seller (tokenId = 2)
await multiPartyTokenizer.reserveToken(
integraHash, 2n, sellerAddress, 1n, processHash
);
// Reserve token for Escrow Agent (tokenId = 3)
await multiPartyTokenizer.reserveTokenAnonymous(
integraHash, 3n, 1n, encryptedLabel, processHash
);Who Can Reserve?
Only two addresses can call reservation functions:
- The record owner -- the address that owns the record in
IntegraRecordV1 - The authorized executor -- a per-record delegate set by the owner
There are no global operator roles. The tokenizer verifies this by calling IntegraRecordV1.getAccessInfo(integraHash) on every reservation.
Step 4: Claim the Token
Claiming is where the token actually gets minted. The claimant must present a valid EAS attestation UID that proves they have the CAPABILITY_CLAIM_TOKEN capability for this specific record.
The Attestation Requirement
The EAS attestation must satisfy these conditions:
- Issued by the record owner or an owner-delegated issuer
- Contains the correct
integraHashandtokenId - Uses the
INTEGRA_CAPABILITY_V1.0.0schema - Is not expired or revoked
- Grants the
CAPABILITY_CLAIM_TOKENcapability bit
Claim Call
// The claimant calls this -- the attestation UID proves their right to claim
tokenizer.claimToken(
integraHash, // record identifier
tokenId, // which token to claim
capabilityAttestationUID, // EAS attestation UID (bytes32)
processHash // workflow correlation ID
);// ethers.js v6 -- called by the token recipient
const tx = await tokenizer.claimToken(
integraHash,
1n, // tokenId
attestationUID, // EAS attestation UID
processHash
);
const receipt = await tx.wait();
console.log("Token claimed and minted!");For named reservations, the caller must match the reservedFor address set during reservation. For anonymous reservations, any address with a valid attestation can claim.
What Happens During Claim
The tokenizer (via its inherited AttestationAccessControlV1) executes this verification pipeline:
- Determines the attestation provider for the record
- Looks up the provider address from
IntegraRegistryV1 - Calls
IAttestationProvider.verifyCapabilities()on the provider - The provider runs a 13-step verification (schema, expiration, revocation, issuer authorization, capability extraction)
- Checks that the claimant holds the required capability bit
- Mints the token to the claimant
Step 5: Use the Token
After claiming, the token is a standard ERC token. You can interact with it through the standard interfaces:
ERC-721 (Single Owner)
// Check ownership
const owner = await tokenizer.ownerOf(tokenId);
// Transfer
await tokenizer.transferFrom(currentOwner, newOwner, tokenId);
// Get metadata URI
const uri = await tokenizer.tokenURI(tokenId);ERC-20 (Fungible Shares)
// Check balance
const balance = await tokenizer.balanceOf(holder);
// Transfer shares
await tokenizer.transfer(recipient, ethers.parseUnits("100", 18));
// Approve and transferFrom (delegated)
await tokenizer.approve(spender, ethers.parseUnits("500", 18));
await tokenizer.connect(spender).transferFrom(holder, recipient, amount);ERC-1155 (Multi-Token)
// Check balance of a specific token type
const balance = await tokenizer.balanceOf(holder, tokenId);
// Transfer
await tokenizer.safeTransferFrom(from, to, tokenId, amount, "0x");
// Batch balance query
const balances = await tokenizer.balanceOfBatch(
[addr1, addr2, addr3],
[1n, 2n, 3n]
);Transfer Restrictions
Most tokenizers allow free transfers after claiming, with these exceptions:
- Soulbound tokenizers (
SoulboundTokenizerV1,LicenseTokenizerV1,MembershipTokenizerV1) -- all transfers are blocked. Tokens are permanently bound to the claiming address. - SecurityTokenTokenizerV1 -- transfers are restricted to whitelisted addresses.
- Paused state -- any tokenizer can be paused by governance, blocking all operations including transfers.
Cancelling a Reservation
Before a token is claimed, the record owner or executor can cancel the reservation:
const tx = await tokenizer.cancelReservation(
integraHash,
1n, // tokenId
processHash
);
await tx.wait();Cancellation is impossible after a token has been claimed. Once minted, the token exists permanently as a standard ERC token.
Complete Example: ERC-721 Ownership Token
Here is the full flow from record creation through tokenization:
import { ethers, keccak256, toUtf8Bytes, randomBytes, AbiCoder } from "ethers";
const provider = new ethers.JsonRpcProvider("https://sepolia.base.org");
const owner = new ethers.Wallet(OWNER_KEY, provider);
const recipient = new ethers.Wallet(RECIPIENT_KEY, provider);
// Contracts
const record = new ethers.Contract(RECORD_ADDRESS, recordAbi, owner);
const tokenizer = new ethers.Contract(OWNERSHIP_TOKENIZER_ADDRESS, tokenizerAbi, owner);
// --- Step 1: Create the record with a tokenizer ---
const contentHash = keccak256(toUtf8Bytes("Property deed for 123 Main St"));
const integraHash = keccak256(
AbiCoder.defaultAbiCoder().encode(
["bytes32", "address", "uint256"],
[contentHash, owner.address, Date.now()]
)
);
const processHash = keccak256(randomBytes(32));
const params = {
integraHash,
contentHash,
identityExtension: ethers.ZeroHash,
referenceHash: ethers.ZeroHash,
tokenizer: OWNERSHIP_TOKENIZER_ADDRESS,
primaryResolverId: ethers.ZeroHash,
authorizedExecutor: ethers.ZeroAddress,
processHash,
paymentToken: ethers.ZeroAddress,
owner: ethers.ZeroAddress,
};
const proof = { a: [0n, 0n], b: [[0n, 0n], [0n, 0n]], c: [0n, 0n] };
await (await record.register(params, proof, {
value: ethers.parseEther("0.001"),
})).wait();
console.log("Record created with tokenizer");
// --- Step 2: Reserve a token for the recipient ---
await (await tokenizer.reserveToken(
integraHash, 1n, recipient.address, 1n, processHash
)).wait();
console.log("Token reserved for", recipient.address);
// --- Step 3: Recipient claims with EAS attestation ---
// (attestation must be created separately via EAS -- see Access Control guide)
const tokenizerAsRecipient = tokenizer.connect(recipient);
await (await tokenizerAsRecipient.claimToken(
integraHash, 1n, attestationUID, processHash
)).wait();
console.log("Token claimed!");
// --- Step 4: Verify ownership ---
const tokenOwner = await tokenizer.ownerOf(1n);
console.log("Token owner:", tokenOwner); // recipient.address