Ownership Transfer Tutorial
Implement secure, blockchain-verified ownership transfers in your application.
How Transfers Work
TAG IT Network uses a secure two-step transfer process to ensure both parties consent to ownership changes. This prevents unauthorized transfers and provides a clear audit trail on the blockchain.
The transfer flow consists of:
- Initiation - The current owner (seller) initiates the transfer
- Verification - The buyer's identity is verified via wallet signature
- Confirmation - The buyer confirms acceptance of the transfer
- Recording - The blockchain record is automatically updated
- Notification - Both parties receive confirmation via webhooks
Transfer requests expire after 72 hours if not confirmed by the buyer. This prevents stale transfers from cluttering the system.
Step 1: Initiate Transfer
The seller (current owner) initiates the transfer by specifying the product and the buyer's wallet address:
import { TagIt } from '@tagit/sdk';
const tagit = new TagIt({
apiKey: process.env.TAGIT_API_KEY,
network: 'mainnet'
});
// Seller initiates the transfer
async function initiateTransfer(tagId, buyerAddress) {
try {
const transfer = await tagit.transfers.initiate({
tagId: tagId,
toAddress: buyerAddress,
metadata: {
salePrice: '2500.00',
currency: 'USD',
notes: 'Luxury watch sale - includes original box and papers'
}
});
console.log('Transfer initiated:', transfer.transferId);
console.log('Status:', transfer.status); // 'pending_acceptance'
console.log('Expires at:', transfer.expiresAt);
return transfer;
} catch (error) {
console.error('Failed to initiate transfer:', error.message);
throw error;
}
}
// Example usage
const transfer = await initiateTransfer(
'NFC-TAG-001-ABC',
'0x8Ba1f109551bD432803012645Ac136ddd64DBA72'
);
Step 2: Verify Buyer Identity
Before the buyer can accept the transfer, they must verify their wallet ownership by signing a message:
// Buyer verifies their identity
async function verifyBuyerIdentity(transferId) {
// Generate a verification challenge
const challenge = await tagit.transfers.getChallenge(transferId);
console.log('Challenge message:', challenge.message);
// "TAG IT Network Transfer Verification\nTransfer ID: xfer_abc123\nTimestamp: 1706123456"
// Buyer signs the challenge with their wallet (e.g., using ethers.js)
const signer = provider.getSigner();
const signature = await signer.signMessage(challenge.message);
// Submit the signature for verification
const verification = await tagit.transfers.verifyIdentity({
transferId: transferId,
signature: signature
});
console.log('Identity verified:', verification.verified);
console.log('Verified address:', verification.address);
return verification;
}
// Example with MetaMask in browser
async function verifyWithMetaMask(transferId) {
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const challenge = await tagit.transfers.getChallenge(transferId);
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [challenge.message, accounts[0]]
});
return await tagit.transfers.verifyIdentity({
transferId,
signature
});
}
Always verify the challenge message content before signing. Never sign messages from untrusted sources, as this could be used in phishing attacks.
Step 3: Confirm Transfer
Once identity is verified, the buyer confirms the transfer to complete the ownership change:
// Buyer confirms the transfer
async function confirmTransfer(transferId) {
try {
const result = await tagit.transfers.confirm({
transferId: transferId,
acceptTerms: true
});
console.log('Transfer confirmed!');
console.log('New owner:', result.newOwner);
console.log('Transaction hash:', result.txHash);
console.log('Block number:', result.blockNumber);
return result;
} catch (error) {
if (error.code === 'TRANSFER_EXPIRED') {
console.error('Transfer has expired. Please request a new transfer.');
} else if (error.code === 'IDENTITY_NOT_VERIFIED') {
console.error('Please verify your identity first.');
} else {
console.error('Transfer confirmation failed:', error.message);
}
throw error;
}
}
// Complete transfer flow for buyer
async function acceptTransfer(transferId) {
// Step 1: Verify identity
await verifyBuyerIdentity(transferId);
// Step 2: Confirm the transfer
const result = await confirmTransfer(transferId);
console.log('Ownership transfer complete!');
return result;
}
Step 4: Update Blockchain Record
After confirmation, the blockchain record is automatically updated. You can query the updated product information:
// Query updated product ownership
async function getProductOwnership(tagId) {
const product = await tagit.products.get(tagId);
console.log('Current owner:', product.currentOwner);
console.log('Transfer count:', product.transferHistory.length);
// Get full transfer history
const history = await tagit.products.getTransferHistory(tagId);
history.forEach((transfer, index) => {
console.log(`Transfer ${index + 1}:`);
console.log(` From: ${transfer.from}`);
console.log(` To: ${transfer.to}`);
console.log(` Date: ${transfer.timestamp}`);
console.log(` TX: ${transfer.txHash}`);
});
return product;
}
// Verify transfer on-chain directly
async function verifyOnChain(tagId, expectedOwner) {
const verification = await tagit.products.verifyOwnership({
tagId: tagId,
address: expectedOwner
});
console.log('Is owner:', verification.isOwner);
console.log('Verified at block:', verification.blockNumber);
return verification;
}
// Example transfer history response
{
"tagId": "NFC-TAG-001-ABC",
"currentOwner": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"transferHistory": [
{
"transferId": "xfer_001",
"from": "0x0000000000000000000000000000000000000000",
"to": "0x742d35Cc6634C0532925a3b844Bc9e7595f5c9E8",
"timestamp": "2024-01-15T10:30:00Z",
"txHash": "0xabc123...",
"type": "mint"
},
{
"transferId": "xfer_002",
"from": "0x742d35Cc6634C0532925a3b844Bc9e7595f5c9E8",
"to": "0x8Ba1f109551bD432803012645Ac136ddd64DBA72",
"timestamp": "2024-06-20T14:45:00Z",
"txHash": "0xdef456...",
"type": "transfer",
"metadata": {
"salePrice": "2500.00",
"currency": "USD"
}
}
]
}
Step 5: Send Notifications
Configure webhooks to receive real-time notifications about transfer events:
// Configure webhook endpoint in your dashboard or via API
async function configureWebhook() {
const webhook = await tagit.webhooks.create({
url: 'https://your-app.com/api/webhooks/tagit',
events: [
'transfer.initiated',
'transfer.verified',
'transfer.confirmed',
'transfer.cancelled',
'transfer.expired'
],
secret: process.env.WEBHOOK_SECRET
});
console.log('Webhook configured:', webhook.id);
return webhook;
}
// Handle incoming webhooks (Express.js example)
import express from 'express';
import crypto from 'crypto';
const app = express();
app.post('/api/webhooks/tagit', express.raw({ type: 'application/json' }), (req, res) => {
// Verify webhook signature
const signature = req.headers['x-tagit-signature'];
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (signature !== `sha256=${expectedSignature}`) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'transfer.initiated':
console.log('Transfer initiated:', event.data.transferId);
// Notify buyer about pending transfer
notifyBuyer(event.data);
break;
case 'transfer.confirmed':
console.log('Transfer confirmed:', event.data.transferId);
// Update your database, send confirmation emails
updateOwnershipRecord(event.data);
sendConfirmationEmails(event.data);
break;
case 'transfer.expired':
console.log('Transfer expired:', event.data.transferId);
// Clean up pending transfers
handleExpiredTransfer(event.data);
break;
}
res.status(200).send('OK');
});
Secondary Market Integration
Integrate with marketplace APIs to enable secondary market sales with authenticated products:
// List a product for sale on supported marketplaces
async function listForSale(tagId, listingDetails) {
// First, verify you own the product
const verification = await tagit.products.verifyOwnership({
tagId: tagId,
address: await signer.getAddress()
});
if (!verification.isOwner) {
throw new Error('You must own the product to list it for sale');
}
// Create marketplace listing
const listing = await tagit.marketplace.createListing({
tagId: tagId,
price: listingDetails.price,
currency: listingDetails.currency,
description: listingDetails.description,
images: listingDetails.images,
shippingOptions: listingDetails.shipping,
acceptedPaymentMethods: ['crypto', 'card']
});
console.log('Listing created:', listing.listingId);
console.log('Marketplace URL:', listing.url);
return listing;
}
// Handle purchase from marketplace
async function handleMarketplacePurchase(listingId, buyerAddress) {
// Marketplace handles payment, then initiates transfer
const purchase = await tagit.marketplace.completePurchase({
listingId: listingId,
buyerAddress: buyerAddress
});
// Transfer is automatically initiated
console.log('Transfer initiated:', purchase.transferId);
console.log('Buyer should confirm at:', purchase.confirmationUrl);
return purchase;
}
// Query marketplace activity for a product
async function getMarketplaceHistory(tagId) {
const history = await tagit.marketplace.getHistory(tagId);
history.sales.forEach(sale => {
console.log(`Sale on ${sale.date}: ${sale.price} ${sale.currency}`);
console.log(` Platform: ${sale.marketplace}`);
console.log(` Verified: ${sale.authenticated}`);
});
return history;
}
Escrow Transfers
For high-value items, use escrow transfers to protect both buyer and seller:
// Create an escrow transfer for high-value items
async function createEscrowTransfer(tagId, buyerAddress, terms) {
const escrow = await tagit.transfers.createEscrow({
tagId: tagId,
toAddress: buyerAddress,
amount: terms.price,
currency: terms.currency,
escrowPeriod: terms.inspectionDays * 24 * 60 * 60, // Convert days to seconds
conditions: {
requirePhysicalDelivery: true,
requireInspectionApproval: true,
disputeResolutionMethod: 'arbitration'
}
});
console.log('Escrow created:', escrow.escrowId);
console.log('Escrow address:', escrow.escrowContractAddress);
console.log('Buyer must deposit:', escrow.requiredDeposit);
return escrow;
}
// Buyer funds the escrow
async function fundEscrow(escrowId) {
const funding = await tagit.escrow.fund({
escrowId: escrowId,
// Payment handled via connected wallet
});
console.log('Escrow funded:', funding.txHash);
console.log('Status:', funding.status); // 'awaiting_delivery'
return funding;
}
// Confirm delivery and release funds
async function confirmDeliveryAndRelease(escrowId) {
// Buyer confirms they received the item in good condition
const release = await tagit.escrow.confirmAndRelease({
escrowId: escrowId,
inspectionPassed: true,
rating: 5,
feedback: 'Item exactly as described, fast shipping!'
});
console.log('Escrow released:', release.txHash);
console.log('Seller received:', release.sellerPayout);
console.log('Ownership transferred:', release.transferComplete);
return release;
}
// Handle dispute if item not as described
async function initiateDispute(escrowId, reason) {
const dispute = await tagit.escrow.initiateDispute({
escrowId: escrowId,
reason: reason,
evidence: [
{ type: 'image', url: 'https://...' },
{ type: 'description', text: 'Item has damage not shown in listing' }
]
});
console.log('Dispute created:', dispute.disputeId);
console.log('Arbitration deadline:', dispute.deadline);
return dispute;
}
For items valued over $1,000, we recommend using escrow transfers to protect both parties and ensure a smooth transaction.