pop402 Integration Guide

Open site

Challenge Authentication

Challenge-response authentication proves wallet ownership without requiring a blockchain transaction.

Flow

  1. Client requests challenge with wallet address
  2. Server generates unique message with expiration
  3. Client signs message with wallet
  4. 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 identifier
  • auth - Authentication type
  • walletAddress - Base58 wallet address
  • expiresAt - Unix timestamp
  • challengeId - 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 exist
  • challenge_expired - TTL passed
  • wallet_mismatch - Wrong wallet signed
  • invalid_signature - Signature verification failed
  • no_license - Valid signature but no license for SKU