Skip to content

License Purchases

This document explains how license purchases work on WasiAI, from the user's click to the NFT mint.


Overview

License NFTs provide unlimited access to AI models. Unlike pay-per-inference, licenses are:

  • One-time or monthly payment
  • Ownership via NFT
  • Tradeable on secondary markets
  • Requires gas for purchase

Purchase Flow

User clicks "Buy License"
Frontend checks USDC approval
    ┌────┴────┐
    │         │
    ▼         ▼
Not Approved  Approved
    │         │
    ▼         │
Approve USDC  │
    │         │
    └────┬────┘
Call MarketplaceV3.buyLicense()
USDC transferred to Splitter
Splitter distributes to:
├── Marketplace (5%)
├── Creator (royalty %)
└── Seller (remainder)
LicenseNFT minted to buyer
User receives License NFT

Smart Contract Interaction

Step 1: USDC Approval

Before first purchase, user must approve USDC spending:

await usdc.approve(
  MARKETPLACE_ADDRESS,
  licensePrice
)

Step 2: Buy License

await marketplace.buyLicense(
  modelId,      // Model to license
  kind,         // 0 = perpetual, 1 = subscription
  rights        // 1 = API, 2 = download, 3 = both
)

Step 3: Internal Flow

Inside buyLicense():

function buyLicense(uint256 modelId, uint8 kind, uint8 rights) external {
    Model storage model = models[modelId];

    // 1. Determine price
    uint256 price = kind == KIND_PERPETUAL 
        ? model.pricePerpetual 
        : model.priceSubscription;

    // 2. Transfer USDC to splitter
    address splitter = splitterFactory.getSplitter(modelId);
    paymentToken.safeTransferFrom(msg.sender, splitter, price);

    // 3. Mint license NFT
    uint256 duration = kind == KIND_PERPETUAL ? 0 : model.defaultDurationDays * 1 days;
    uint256 licenseId = licenseNFT.mint(msg.sender, modelId, kind, rights, duration);

    // 4. Emit event
    emit LicensePurchased(modelId, licenseId, msg.sender, kind, price);
}

License Types

Perpetual (KIND_PERPETUAL = 0)

  • Payment: One-time
  • Duration: Forever (expiresAt = 0)
  • Best for: Long-term projects, heavy users

Subscription (KIND_SUBSCRIPTION = 1)

  • Payment: Per period
  • Duration: 30 days (configurable)
  • Renewable: Yes
  • Best for: Testing, short-term needs

License Rights

Rights are stored as a bitmask:

Value Binary Rights
1 01 API Access only
2 10 Download Access only
3 11 API + Download

Checking Rights

// In LicenseNFTV2
function hasRights(uint256 tokenId, uint8 required) external view returns (bool) {
    return (licenses[tokenId].rights & required) == required;
}

Payment Distribution

When USDC reaches the splitter:

License Price: $100
┌─────────────────────────────────────┐
│         ModelSplitter               │
│                                     │
│  Marketplace (5%): $5.00            │
│  Creator (10%): $9.50               │
│  Seller (85%): $85.50               │
│                                     │
└─────────────────────────────────────┘

Distribution happens automatically when distribute() is called.


Frontend Implementation

React Component

function BuyLicenseButton({ modelId, kind, price }) {
  const { address } = useAccount()
  const [isApproved, setIsApproved] = useState(false)

  // Check approval
  useEffect(() => {
    const checkApproval = async () => {
      const allowance = await usdc.allowance(address, MARKETPLACE_ADDRESS)
      setIsApproved(allowance >= price)
    }
    checkApproval()
  }, [address, price])

  const handleApprove = async () => {
    await usdc.approve(MARKETPLACE_ADDRESS, price)
    setIsApproved(true)
  }

  const handleBuy = async () => {
    await marketplace.buyLicense(modelId, kind, RIGHTS_API)
  }

  if (!isApproved) {
    return <Button onClick={handleApprove}>Approve USDC</Button>
  }

  return <Button onClick={handleBuy}>Buy License</Button>
}

Gas Costs

Action Estimated Gas Estimated Cost
USDC Approval ~46,000 ~$0.02
Buy License ~150,000 ~$0.06
Total (first time) ~196,000 ~$0.08
Total (subsequent) ~150,000 ~$0.06

Verifying Licenses

On-Chain Check

// Check if user has valid license for model
async function hasValidLicense(user: string, modelId: number): Promise<boolean> {
  const licenses = await licenseNFT.getLicensesByOwner(user)

  for (const licenseId of licenses) {
    const license = await licenseNFT.licenses(licenseId)

    if (
      license.modelId === modelId &&
      await licenseNFT.isValid(licenseId)
    ) {
      return true
    }
  }

  return false
}

In Inference API

// Skip payment if user has valid license
const hasLicense = await hasValidLicense(userAddress, modelId)

if (hasLicense) {
  // Run inference without payment
  return runInference(input)
}

// Otherwise, require x402 payment
return new Response(null, { status: 402, ... })

Error Handling

Common Errors

Error Cause Solution
InsufficientFunds Not enough USDC Get more USDC
NotListed Model is delisted Choose different model
PriceNotConfigured License type not available Try different type
TransferFailed USDC transfer failed Check approval

Frontend Error Handling

try {
  await marketplace.buyLicense(modelId, kind, rights)
} catch (error) {
  if (error.message.includes('InsufficientFunds')) {
    showError('Not enough USDC')
  } else if (error.message.includes('user rejected')) {
    showError('Transaction cancelled')
  } else {
    showError('Purchase failed')
  }
}