Skip to main content

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
  1. Reserve -- The record owner (or authorized executor) allocates a token slot without minting.
  2. Claim -- The intended recipient presents a valid EAS attestation and the token is minted to them.
  3. 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.

TokenizerBest For
OwnershipTokenizerV1General single-owner, transferable assets
SoulboundTokenizerV1Non-transferable credentials (diplomas, certifications)
LicenseTokenizerV1Software/professional licenses with expiration
EscrowTokenizerV1Escrow agreements with conditional release
DebtTokenizerV1Debt instruments, promissory notes
InvoiceTokenizerV1Invoices and trade receivables

ERC-20: Fungible Shares Per Record

Best for fractional ownership or governance -- each token is interchangeable.

TokenizerBest For
SharesTokenizerV1Company equity, partnership interests
GovernanceTokenizerV1DAO governance with delegation and checkpoints
SecurityTokenTokenizerV1Regulated securities with transfer restrictions
FractionalTokenizerV1Fractional ownership with buyout/voting

ERC-1155: Multiple Tokens Per Record

Best for multi-party agreements where each participant gets a distinct token.

TokenizerBest For
MultiPartyTokenizerV1Multi-stakeholder agreements (closings, joint ventures)
RoyaltyTokenizerV1Royalty splits, revenue sharing
BadgeTokenizerV1Achievement badges, credentials
TrustTokenizerV1Trust agreements with multiple beneficiary classes
RentalTokenizerV1Time-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:

  1. The record owner -- the address that owns the record in IntegraRecordV1
  2. 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 integraHash and tokenId
  • Uses the INTEGRA_CAPABILITY_V1.0.0 schema
  • Is not expired or revoked
  • Grants the CAPABILITY_CLAIM_TOKEN capability 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:

  1. Determines the attestation provider for the record
  2. Looks up the provider address from IntegraRegistryV1
  3. Calls IAttestationProvider.verifyCapabilities() on the provider
  4. The provider runs a 13-step verification (schema, expiration, revocation, issuer authorization, capability extraction)
  5. Checks that the claimant holds the required capability bit
  6. 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

Next Steps