Skip to main content

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 eligible
  • discount_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 eligible status
  • Respond within timeout (default: 3 seconds)
  • Handle errors gracefully (return eligible: false on 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

  1. Navigate to Perk Creation/Edit
  2. Enable "Purchase Constraints"
  3. Add constraint → Select "External API"
  4. Enter your API URL (must be HTTPS)
  5. Configure authentication (Bearer token, API key, etc.)
  6. Set timeout and cache TTL
  7. 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

  1. HTTPS Only: External API endpoints must use HTTPS
  2. Authentication: Use Bearer tokens or API keys for your endpoints
  3. Rate Limiting: Implement rate limiting on your endpoints
  4. Input Validation: Validate and sanitize all inputs
  5. Error Handling: Return eligible: false on errors (fail closed)
  6. Logging: Log validation requests for auditing

For Loyalteez Platform

  1. On-chain Verification: NFT ownership always verified on-chain
  2. Encryption: API keys encrypted in database
  3. Timeout Protection: Configurable timeouts prevent hanging requests
  4. Cache Invalidation: Automatic cache updates on state changes
  5. Fail Closed: If validation fails, user is not eligible

Troubleshooting

Constraint Validation Fails

Issue: User cannot purchase perk even though they meet requirements

Solutions:

  1. Check cache - may need to wait for TTL or manually invalidate
  2. Verify Discord account is linked to wallet
  3. Check NFT ownership on-chain
  4. Review external API logs for errors
  5. Check constraint configuration in partner portal

External API Timeout

Issue: External API constraint times out

Solutions:

  1. Increase api_timeout_ms (max: 10 seconds)
  2. Optimize your API endpoint performance
  3. Check API endpoint availability
  4. Consider using cache invalidation webhooks instead of real-time checks

Cache Not Updating

Issue: Changes not reflected in constraint validation

Solutions:

  1. Wait for cache TTL expiration
  2. Manually invalidate cache via webhook
  3. Check constraint updater worker is running
  4. 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