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:
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 |