Challenge Authentication
Challenge-response authentication proves wallet ownership without requiring a blockchain transaction.
Flow
- Client requests challenge with wallet address
- Server generates unique message with expiration
- Client signs message with wallet
- Server verifies signature and checks license
API
POST /challenge
Request a challenge for authentication.
Request:
{
"walletAddress": "CqwZ6fXzyPNLf9WdD2koezjk91AiDc4VH3Vk3BPWyjh",
"ttl": 3600
}
Response:
{
"challenge": {
"id": "chal_7x9k2m3p5q8r",
"message": "pop402:auth:CqwZ6fXzy...:1730123456:chal_7x9k2m3p5q8r",
"expiresAt": 1730123456,
"expiresIn": 3600
}
}
POST /verify
Verify signed challenge and check license.
Request:
{
"paymentPayload": {
"x402Meta": {
"challengeId": "chal_7x9k2m3p5q8r",
"signature": "5Kd8f2HjP9mN3qR...",
"walletAddress": "CqwZ6fXzyPNLf9WdD2...",
"sku": "premium-content-xyz"
}
},
"paymentRequirements": {
"network": "solana"
}
}
Response:
{
"isValid": true,
"payer": "CqwZ6fXzyPNLf9WdD2..."
}
Message Format
pop402:auth:{walletAddress}:{expiresAt}:{challengeId}
pop402- Protocol identifierauth- Authentication typewalletAddress- Base58 wallet addressexpiresAt- Unix timestampchallengeId- Unique nonce
Example
async function authenticate(sku: string) {
const wallet = await window.solana.connect();
// 1. Get challenge
const res = await fetch('https://facilitator.pop402.com/challenge', {
method: 'POST',
body: JSON.stringify({
walletAddress: wallet.publicKey.toBase58(),
ttl: 3600
})
});
const { challenge } = await res.json();
// 2. Sign
const message = new TextEncoder().encode(challenge.message);
const signature = await wallet.signMessage(message);
// 3. Verify
const verify = await fetch('https://facilitator.pop402.com/verify', {
method: 'POST',
body: JSON.stringify({
paymentPayload: {
x402Meta: {
challengeId: challenge.id,
signature: bs58.encode(signature),
walletAddress: wallet.publicKey.toBase58(),
sku: sku
}
},
paymentRequirements: { network: 'solana' }
})
});
return await verify.json();
}
Configuration
Environment variables:
CHALLENGE_TTL_SEC=300 # Challenge expiration (default: 5 min)
JWT_SECRET=your-secret # For session tokens
SESSION_TTL_SECONDS=3600 # Session expiration (default: 1 hour)
Security
- HTTPS required
- Challenges expire after TTL
- Challenges are reusable until expiration (session model)
- Each challenge has unique nonce
- Signature cryptographically proves wallet ownership
Errors
challenge_not_found- Challenge expired or doesn't existchallenge_expired- TTL passedwallet_mismatch- Wrong wallet signedinvalid_signature- Signature verification failedno_license- Valid signature but no license for SKU