Overview
Feature Unlock products let users purchase access to premium features in your application. PayGate generates access tokens that you verify via API.How It Works
Copy
┌─────────────────────────────────────────────────────────────┐
│ Feature Unlock Flow │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. User purchases feature unlock │
│ └─► Pays via x402 │
│ │
│ 2. PayGate generates access token │
│ └─► Format: ft_xxxxxxxxxxxx │
│ │
│ 3. Webhook sent to your server │
│ └─► Contains token + feature IDs │
│ │
│ 4. Store token for user │
│ └─► Associate with wallet/account │
│ │
│ 5. User provides token in your app │
│ └─► Or auto-retrieve from wallet │
│ │
│ 6. Your app verifies via API │
│ └─► GET /public/feature/verify │
│ │
│ 7. Enable features if valid │
│ └─► Check feature IDs │
│ │
└─────────────────────────────────────────────────────────────┘
Creating a Feature Unlock Product
Dashboard
- Go to Products → Create Product
- Select type: Feature Unlock
- Fill in details:
Copy
Title: Pro Plan
Description: Unlock all premium features
Price: 10.00 USDC
Network: mainnet-beta
Feature IDs:
- pro_analytics
- pro_export
- pro_themes
- unlimited_storage
Webhook Integration
When a user purchases, you receive:Copy
{
"type": "feature.unlocked",
"productId": "prod_pro123",
"productTitle": "Pro Plan",
"accessToken": "ft_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"featureIds": [
"pro_analytics",
"pro_export",
"pro_themes",
"unlimited_storage"
],
"walletAddress": "7xK3abcdefghijklmnop",
"txHash": "5xyz...",
"timestamp": "2024-01-15T10:30:00.000Z"
}
Handling the Webhook
Copy
app.post('/api/paygate-webhook', async (req, res) => {
const { type, accessToken, featureIds, walletAddress } = req.body;
if (type === 'feature.unlocked') {
// Find or create user
let user = await db.users.findUnique({
where: { wallet: walletAddress }
});
if (!user) {
user = await db.users.create({
data: { wallet: walletAddress }
});
}
// Store access token and features
await db.users.update({
where: { id: user.id },
data: {
accessToken,
features: featureIds,
upgradedAt: new Date()
}
});
// Send confirmation
await sendNotification(user, 'Pro features unlocked!');
}
res.json({ received: true });
});
Verifying Access Tokens
API Endpoint
Copy
POST https://api-paygate.getaether.xyz/public/feature/verify
Request
Copy
const response = await fetch(
'https://api-paygate.getaether.xyz/public/feature/verify',
{
method: 'POST',
headers: {
'X-API-Key': process.env.PAYGATE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
accessToken: 'ft_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
})
}
);
const result = await response.json();
Response
Copy
{
"valid": true,
"features": [
"pro_analytics",
"pro_export",
"pro_themes",
"unlimited_storage"
],
"productId": "prod_pro123",
"walletAddress": "7xK3abcdefghijklmnop",
"createdAt": "2024-01-15T10:30:00.000Z"
}
Invalid Token Response
Copy
{
"valid": false,
"error": "Token not found or revoked"
}
Integration Patterns
Middleware Pattern
Copy
async function requireFeature(featureId: string) {
return async (req, res, next) => {
const token = req.headers['x-access-token'];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
const response = await fetch(
'https://api-paygate.getaether.xyz/public/feature/verify',
{
method: 'POST',
headers: {
'X-API-Key': process.env.PAYGATE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ accessToken: token })
}
);
const { valid, features } = await response.json();
if (!valid || !features.includes(featureId)) {
return res.status(403).json({
error: 'Feature not available',
upgrade: `https://paygate.getaether.xyz/pay/prod_pro123`
});
}
next();
};
}
// Usage
app.get('/api/analytics',
requireFeature('pro_analytics'),
async (req, res) => {
// Pro analytics logic
}
);
React Hook
Copy
import { useState, useEffect } from 'react';
function useFeatures(accessToken: string | null) {
const [features, setFeatures] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!accessToken) {
setFeatures([]);
setLoading(false);
return;
}
async function verify() {
try {
const response = await fetch('/api/verify-features', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessToken })
});
const { valid, features } = await response.json();
if (valid) {
setFeatures(features);
}
} finally {
setLoading(false);
}
}
verify();
}, [accessToken]);
return {
features,
loading,
hasFeature: (id: string) => features.includes(id)
};
}
// Usage
function App() {
const { hasFeature, loading } = useFeatures(user?.accessToken);
if (loading) return <Loading />;
return (
<div>
{hasFeature('pro_analytics') ? (
<ProAnalytics />
) : (
<UpgradePrompt />
)}
</div>
);
}
Managing Tokens
List All Tokens
Copy
const response = await fetch(
'https://api-paygate.getaether.xyz/public/feature/tokens',
{
headers: {
'X-API-Key': process.env.PAYGATE_API_KEY
}
}
);
const { tokens } = await response.json();
// Returns all tokens for your products
Revoke a Token
Copy
await fetch(
'https://api-paygate.getaether.xyz/public/feature/revoke',
{
method: 'POST',
headers: {
'X-API-Key': process.env.PAYGATE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
accessToken: 'ft_token_to_revoke'
})
}
);
Best Practices
- Cache verification - Don’t verify on every request
- Handle revocation - Listen for token revocation events
- Graceful degradation - Show upgrade prompts for missing features
- Offline access - Consider local caching for verified features
- Security - Never expose access tokens to client-side code
