Purchase Constraints
Purchase constraints allow brands to gate perk purchases based on various conditions like Discord levels, roles, NFT ownership, or custom validation endpoints. This enables sophisticated gating logic while maintaining fast validation through caching.
Overview
The purchase constraints system provides:
- Discord-based constraints: Require specific Discord levels or roles
- NFT ownership constraints: Require ownership of Loyalteez perks or external ERC721 NFTs
- External API constraints: Custom validation via brand's own endpoint
- Dynamic pricing: Apply discounts based on user qualifications
- Fast validation: KV-based caching for high performance
- Security: On-chain NFT verification for critical checks
Constraint Types
Discord Level Constraint
Require users to have a minimum Discord level in a specific server.
Configuration:
{
type: 'discord_level',
server_id: '123456789012345678',
min_level: 5,
operator: 'AND' // Default: AND
}
Requirements:
- User must have their Discord account linked to their wallet
- User must be in the specified Discord server
- User's level must meet or exceed
min_level
Discord Role Constraint
Require users to have a specific Discord role.
Configuration:
{
type: 'discord_role',
server_id: '123456789012345678',
role_id: '987654321098765432',
operator: 'AND' // Default: AND
}
Requirements:
- User must have their Discord account linked to their wallet
- User must be in the specified Discord server
- User must have the specified role
NFT Ownership Constraint
Require users to own specific NFTs (Loyalteez perks or external ERC721 collections).
Loyalteez Perk:
{
type: 'has_perk',
is_loyalteez_perk: true,
collection_id: 123,
token_id: undefined, // Optional: require specific token ID
operator: 'AND'
}
External ERC721 NFT:
{
type: 'has_perk',
is_loyalteez_perk: false,
contract_address: '0x1234567890123456789012345678901234567890',
token_id: undefined, // Optional: require specific token ID
operator: 'AND'
}
Security Note: All NFT ownership checks are always verified on-chain, even if cached results exist. This ensures security against NFT transfers between cache updates.
External API Constraint
Use a brand's own validation endpoint for custom constraint logic.
Configuration:
{
type: 'external_api',
api_url: 'https://your-api.com/validate',
api_method: 'GET', // or 'POST'
api_auth_type: 'bearer', // 'none', 'bearer', 'api_key', or 'basic'
api_auth_value: 'your-api-key', // Encrypted in database
api_headers: {}, // Optional additional headers
api_request_payload: {}, // Optional payload for POST
api_response_path: 'eligible', // JSON path to boolean (e.g., 'data.eligible')
api_timeout_ms: 3000, // Default: 3000ms
cache_ttl_seconds: 60, // Default: 60 seconds
operator: 'AND'
}
Request Format:
For GET requests:
GET https://your-api.com/validate?wallet=0x...&perk=123
For POST requests:
POST https://your-api.com/validate
Content-Type: application/json
{
"wallet_address": "0x...",
"collection_id": "123",
"timestamp": "2024-01-01T00:00:00.000Z",
...custom fields from api_request_payload
}
Response Format:
Your API should return a JSON response with a boolean eligibility status:
{
"eligible": true
}
Or with nested path:
{
"data": {
"eligible": true,
"reason": null,
"discount_percentage": 20,
"final_price": 800
}
}
Required Fields:
eligible(boolean): Whether the user meets the constraint
Optional Fields:
reason(string): Explanation if not eligiblediscount_percentage(number): Discount percentage (0-100)final_price(number): Final price after discount
Dynamic Pricing
You can apply discounts based on constraint conditions.
Pricing Rule Configuration:
{
condition: {
type: 'discord_role',
server_id: '123456789012345678',
role_id: '987654321098765432'
},
discount_type: 'percentage', // or 'fixed'
value: 20 // 20% off, or fixed LTZ amount
}
Discount Types:
percentage: Discount as percentage (0-100)fixed: Fixed LTZ amount off
Example:
- Base price: 1000 LTZ
- VIP role discount: 20% off
- Final price: 800 LTZ
Constraint Operators
Multiple constraints can be combined with operators:
- AND: All constraints must be met (default)
- OR: At least one constraint must be met
Example with AND:
[
{ type: 'discord_level', min_level: 5, operator: 'AND' },
{ type: 'discord_role', role_id: '...', operator: 'AND' }
]
// User must have Level 5 AND the role
**Example with OR:**
```typescript
[
{ type: 'discord_level', min_level: 5, operator: 'OR' },
{ type: 'has_perk', collection_id: 123, operator: 'OR' }
]
// User must have Level 5 OR own perk collection #123
Cache Strategy
The system uses Cloudflare KV for fast constraint validation:
- Discord constraints: 5 minutes TTL
- NFT constraints: 30 seconds TTL (always verified on-chain)
- External API: Configurable TTL (default: 60 seconds)
Cache is automatically invalidated when:
- Discord user gains/loses role
- Discord user levels up
- NFT is transferred
- Perk constraints are updated
- Brand triggers manual invalidation
Setting Up External API Constraints
1. Implement Your Endpoint
Your endpoint must:
- Accept GET or POST requests
- Use HTTPS (required)
- Return JSON with boolean
eligiblestatus - Respond within timeout (default: 3 seconds)
- Handle errors gracefully (return
eligible: falseon errors)
Node.js Example:
app.get('/validate', async (req, res) => {
const { wallet, perk } = req.query;
// Your custom validation logic
const eligible = await checkUserEligibility(wallet, perk);
res.json({
eligible,
reason: eligible ? null : 'User does not meet requirements',
discount_percentage: eligible ? 15 : 0
});
});
Python Example:
from flask import Flask, request, jsonify
@app.route('/validate', methods=['GET'])
def validate():
wallet = request.args.get('wallet')
perk = request.args.get('perk')
# Your custom validation logic
eligible = check_user_eligibility(wallet, perk)
return jsonify({
'eligible': eligible,
'reason': None if eligible else 'User does not meet requirements',
'discount_percentage': 15 if eligible else 0
})
2. Configure in Partner Portal
- Navigate to Perk Creation/Edit
- Enable "Purchase Constraints"
- Add constraint → Select "External API"
- Enter your API URL (must be HTTPS)
- Configure authentication (Bearer token, API key, etc.)
- Set timeout and cache TTL
- Test connection
3. Handle Cache Invalidation
When your validation logic changes, invalidate the cache:
POST https://constraint-updater.your-domain.workers.dev/invalidate-external-api
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"api_url": "https://your-api.com/validate",
"user_address": "0x...", // Optional: specific user
"collection_id": 123 // Optional: specific perk
}
Security Best Practices
For Brands
- HTTPS Only: External API endpoints must use HTTPS
- Authentication: Use Bearer tokens or API keys for your endpoints
- Rate Limiting: Implement rate limiting on your endpoints
- Input Validation: Validate and sanitize all inputs
- Error Handling: Return
eligible: falseon errors (fail closed) - Logging: Log validation requests for auditing
For Loyalteez Platform
- On-chain Verification: NFT ownership always verified on-chain
- Encryption: API keys encrypted in database
- Timeout Protection: Configurable timeouts prevent hanging requests
- Cache Invalidation: Automatic cache updates on state changes
- Fail Closed: If validation fails, user is not eligible
Troubleshooting
Constraint Validation Fails
Issue: User cannot purchase perk even though they meet requirements
Solutions:
- Check cache - may need to wait for TTL or manually invalidate
- Verify Discord account is linked to wallet
- Check NFT ownership on-chain
- Review external API logs for errors
- Check constraint configuration in partner portal
External API Timeout
Issue: External API constraint times out
Solutions:
- Increase
api_timeout_ms(max: 10 seconds) - Optimize your API endpoint performance
- Check API endpoint availability
- Consider using cache invalidation webhooks instead of real-time checks
Cache Not Updating
Issue: Changes not reflected in constraint validation
Solutions:
- Wait for cache TTL expiration
- Manually invalidate cache via webhook
- Check constraint updater worker is running
- Verify cache invalidation webhooks are configured
API Reference
Validate Constraints
GET /validate?collectionId={id}&userAddress={address}
Response:
{
"eligible": true,
"reason": null,
"finalPrice": "800",
"appliedDiscounts": [
{
"type": "percentage",
"value": 20
}
],
"checks": {
"discord_level": {
"met": true,
"current": 7,
"required": 5
},
"discord_role": {
"met": true,
"hasRole": true
}
}
}
Invalidate Cache
POST /invalidate-external-api
Content-Type: application/json
{
"api_url": "https://your-api.com/validate",
"user_address": "0x...",
"collection_id": 123
}
See Also
- Webhooks Guide - Cache invalidation webhooks
- Custom Events Guide - Event-driven rewards
- Perks Service - Perk management