Skip to Content
All memories

Building a Real-World Asset Tokenization Platform with Django and Web3

 — #Django#Web3#Blockchain#Crypto

Introduction

Real-World Asset (RWA) tokenization is one of the most compelling use cases for blockchain technology — representing physical assets like real estate, commodities, or intellectual property as digital tokens on a blockchain. This unlocks fractional ownership, 24/7 trading, and programmable compliance.

I led the backend architecture for Karpous, a platform that enables users to invest in tokenized real-world assets. The biggest technical challenge? Building a hybrid system that seamlessly bridges traditional web infrastructure (Django, PostgreSQL) with decentralized blockchain protocols (Web3, MetaMask, smart contracts).

Architecture: Centralized Meets Decentralized

┌────────────────────────────────────────────────┐
│                  Frontend (React)                │
├────────────────────────────────────────────────┤
│              Django REST Backend                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────────┐ │
│  │ Auth &   │  │ Asset    │  │ Transaction  │ │
│  │ KYC      │  │ Manager  │  │ Engine       │ │
│  └──────────┘  └──────────┘  └──────────────┘ │
├────────────────────────────────────────────────┤
│              Blockchain Layer                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────────┐ │
│  │ Web3.py  │  │ Smart    │  │ MetaMask     │ │
│  │ Gateway  │  │ Contracts│  │ Integration  │ │
│  └──────────┘  └──────────┘  └──────────────┘ │
├────────────────────────────────────────────────┤
│  PostgreSQL  │  Redis  │  gRPC Services        │
└────────────────────────────────────────────────┘

The Hybrid Challenge

The fundamental tension in RWA platforms is: blockchain transactions are immutable and asynchronous, while your Django app expects synchronous, reversible database operations.

Transaction States

We needed a state machine to track the lifecycle of every tokenization event:

class TokenTransaction(models.Model):
    class Status(models.TextChoices):
        PENDING = 'pending'          # Created in Django
        SUBMITTED = 'submitted'      # Sent to blockchain
        CONFIRMING = 'confirming'    # Waiting for confirmations
        CONFIRMED = 'confirmed'      # On-chain confirmed
        FAILED = 'failed'            # Transaction reverted

    asset = models.ForeignKey('Asset', on_delete=models.PROTECT)
    user = models.ForeignKey('User', on_delete=models.PROTECT)
    tx_hash = models.CharField(max_length=66, null=True, blank=True)
    status = models.CharField(max_length=20, choices=Status.choices)
    amount = models.DecimalField(max_digits=24, decimal_places=8)
    created_at = models.DateTimeField(auto_now_add=True)
    confirmed_at = models.DateTimeField(null=True)

Web3 Integration

We built a gateway service to interact with smart contracts:

# services/web3_gateway.py
from web3 import Web3

class Web3Gateway:
    def __init__(self):
        self.w3 = Web3(Web3.HTTPProvider(settings.RPC_URL))
        self.contract = self.w3.eth.contract(
            address=settings.TOKEN_CONTRACT_ADDRESS,
            abi=TOKEN_ABI
        )

    def mint_tokens(self, to_address: str, amount: int, asset_id: int):
        """Mint RWA tokens to a user's wallet."""
        tx = self.contract.functions.mint(
            to_address, amount, asset_id
        ).build_transaction({
            'from': settings.ADMIN_WALLET,
            'nonce': self.w3.eth.get_transaction_count(
                settings.ADMIN_WALLET
            ),
            'gas': 200000,
            'gasPrice': self.w3.eth.gas_price,
        })

        signed = self.w3.eth.account.sign_transaction(
            tx, settings.ADMIN_PRIVATE_KEY
        )
        tx_hash = self.w3.eth.send_raw_transaction(
            signed.raw_transaction
        )
        return tx_hash.hex()

The Confirmation Problem

Blockchain transactions aren't instant. We used Celery workers to poll for confirmations:

# tasks.py
from celery import shared_task

@shared_task(bind=True, max_retries=50)
def check_transaction_confirmation(self, transaction_id: int):
    tx = TokenTransaction.objects.get(id=transaction_id)

    try:
        receipt = web3_gateway.w3.eth.get_transaction_receipt(tx.tx_hash)

        if receipt is None:
            # Not mined yet — retry in 15 seconds
            raise self.retry(countdown=15)

        if receipt['status'] == 1:
            tx.status = TokenTransaction.Status.CONFIRMED
            tx.confirmed_at = timezone.now()
            tx.save()
            notify_user_confirmation(tx)
        else:
            tx.status = TokenTransaction.Status.FAILED
            tx.save()
            handle_failed_transaction(tx)

    except TransactionNotFound:
        raise self.retry(countdown=15)

Security Considerations

Wallet Verification

Users connect their MetaMask wallet and sign a message to prove ownership:

# views.py
from eth_account.messages import defunct_hash_message

class WalletVerifyView(APIView):
    def post(self, request):
        address = request.data['address']
        signature = request.data['signature']
        message = f"Verify wallet for Karpous: {request.user.id}"

        # Recover signer address from signature
        message_hash = defunct_hash_message(text=message)
        recovered = w3.eth.account.recover_message(
            message_hash, signature=signature
        )

        if recovered.lower() == address.lower():
            request.user.wallet_address = address
            request.user.wallet_verified = True
            request.user.save()
            return Response({"status": "verified"})

        return Response({"error": "Invalid signature"}, status=400)

Double-Spend Prevention

We use database-level locking to prevent race conditions:

from django.db import transaction

@transaction.atomic
def purchase_tokens(user, asset, amount):
    asset = Asset.objects.select_for_update().get(id=asset.id)

    if asset.available_supply < amount:
        raise InsufficientSupplyError()

    asset.available_supply -= amount
    asset.save()

    tx = TokenTransaction.objects.create(
        user=user, asset=asset,
        amount=amount, status='pending'
    )

    tx_hash = web3_gateway.mint_tokens(
        user.wallet_address, amount, asset.id
    )
    tx.tx_hash = tx_hash
    tx.status = 'submitted'
    tx.save()

    check_transaction_confirmation.delay(tx.id)
    return tx

gRPC for Internal Services

For high-frequency internal communication between microservices, we used gRPC instead of REST:

service AssetService {
    rpc GetAssetPrice (AssetRequest) returns (PriceResponse);
    rpc StreamPriceUpdates (AssetRequest) returns (stream PriceResponse);
}

This gave us ~10x lower latency compared to REST for price feed updates.

Lessons Learned

  1. Never trust the frontend for blockchain state — always verify on-chain
  2. Idempotency is critical — network failures mean you might submit a transaction twice
  3. Gas estimation is tricky — always add a buffer and implement retry logic
  4. Test on testnets extensively — mainnet debugging is expensive
  5. Separate hot and cold wallets — the admin wallet for minting should have limited funds

Conclusion

Building Karpous taught me that the hardest part of Web3 development isn't the blockchain itself — it's the glue between your traditional backend and the decentralized world. Django's robust ORM, transaction management, and async task processing (Celery) make it an excellent foundation for hybrid Web3 applications.


This post is based on my work at Orbit Tech Solution building the Karpous RWA platform.