Skip to main content
The Agora API supports two authentication methods: API keys for server-to-server communication and JWT tokens for wallet-based authentication.

Authentication Methods

All API requests must include an Authorization header:
Authorization: Bearer YOUR_API_KEY_OR_JWT

API Key Authentication

API keys are recommended for server-to-server integrations. Keys are hashed using SHA-256 before storage. Location in code: src/app/lib/auth/serverAuth.ts:32
export async function authenticateApiUser(
  request: NextRequest
): Promise<AuthInfo> {
  const authResponse: AuthInfo = await validateBearerToken(request);
  
  if (!authResponse.authenticated) {
    return authResponse;
  }
  
  // Lookup hashed API key
  const user = await prisma.api_user.findFirst({
    where: {
      api_key: hashApiKey(key),
    },
  });
  
  if (!user || !user.enabled) {
    return { authenticated: false, failReason: "Invalid or disabled user" };
  }
  
  return { authenticated: true, userId: user.id };
}

JWT Token Authentication

JWT tokens are used for wallet-based authentication and SIWE (Sign-In with Ethereum). Location in code: src/app/lib/auth/serverAuth.ts:80
export async function generateJwt(
  userId: string,
  roles?: string[] | null,
  ttl?: number | null,
  siweData?: SiweData | null
): Promise<string> {
  const scope = (roles || []).join(";");
  const resolvedTtl = ttl || 60 * 60 * 24; // 24 hours default
  const iat = Math.floor(Date.now() / 1000);
  const exp = iat + resolvedTtl;

  const payload: JWTPayload = {
    sub: userId,
    scope: scope,
    isBadgeholder: scope.includes(ROLE_BADGEHOLDER),
    isCitizen: scope.includes(ROLE_CITIZEN),
    siwe: siweData ? { ...siweData } : undefined,
  };

  return new SignJWT({ ...payload })
    .setProtectedHeader({ alg: "HS256", typ: "JWT" })
    .setExpirationTime(exp)
    .setIssuedAt(iat)
    .sign(new TextEncoder().encode(process.env.JWT_SECRET));
}

API Key Generation

Agora staff can generate API keys using the CLI command:
npm run generate-apikey -- \
  --email [email protected] \
  --address 0x1234567890abcdef1234567890abcdef12345678 \
  --chain-id 1 \
  --description "API access for XYZ integration"

Parameters

email
string
required
Email address of the API user
address
string
required
Ethereum address of the API user
chain-id
number
required
Chain ID (1 for mainnet, 10 for Optimism, etc.)
description
string
required
Description of the API key usage

Adding New Chains

If you need an API key for a new chain, run this SQL against production:
INSERT INTO agora."chain" (id, name, created_at, updated_at) 
VALUES ('10', 'Optimism', '2025-06-09', '2025-06-09');
Where id is the chain ID.

Sign-In with Ethereum (SIWE)

SIWE allows users to authenticate using their wallet signature.

Enable SIWE

Set the environment variable:
NEXT_PUBLIC_SIWE_ENABLED=true

SIWE Flow

  1. Request Nonce: Client requests a nonce from the server
  2. Sign Message: User signs a structured message with their wallet
  3. Verify Signature: Server verifies the signature and issues a JWT
  4. Use JWT: Client includes JWT in subsequent requests

SIWE Message Structure

type SiweData = {
  address: string;
  chainId: string;
  nonce: string;
};

Example SIWE Implementation

import { SiweMessage } from 'siwe';

// 1. Create SIWE message
const message = new SiweMessage({
  domain: window.location.host,
  address: walletAddress,
  statement: 'Sign in to Agora',
  uri: window.location.origin,
  version: '1',
  chainId: 1,
  nonce: await getNonce(), // Get from server
});

// 2. Sign message with wallet
const signature = await wallet.signMessage(message.prepareMessage());

// 3. Verify and get JWT
const response = await fetch('/api/auth/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message, signature }),
});

const { token } = await response.json();

// 4. Use JWT for authenticated requests
const data = await fetch('/api/v1/proposals', {
  headers: { 'Authorization': `Bearer ${token}` },
});

Roles and Permissions

The API supports role-based access control (RBAC): Location in code: src/app/lib/auth/serverAuth.ts:129
export async function getRolesForUser(
  userId: string,
  siweData?: SiweMessage | null
): Promise<string[]> {
  const roles = [ROLE_PUBLIC_READER];
  
  if (siweData) {
    roles.push(ROLE_RF_DEMO_USER); // All SIWE users
    
    const { isBadgeholder, votingCategory, isCitizen } = 
      await fetchBadgeholder(siweData.address);
    
    if (votingCategory) roles.push(votingCategory);
    if (isBadgeholder) roles.push(ROLE_BADGEHOLDER);
    if (isCitizen) roles.push(ROLE_CITIZEN);
  }
  
  return roles;
}

Available Roles

RoleDescription
public_readerRead access to public data (default)
badgeholderRetro Funding badgeholder permissions
citizenCitizen house voting permissions
rf_demo_userRetro Funding demo access
category:*Category-specific permissions

JWT Payload

{
  "sub": "0x1234...5678",
  "scope": "public_reader;badgeholder;category:GOVERNANCE",
  "isBadgeholder": true,
  "isCitizen": false,
  "category": "GOVERNANCE",
  "siwe": {
    "address": "0x1234...5678",
    "chainId": "1",
    "nonce": "abc123"
  },
  "iat": 1640000000,
  "exp": 1640086400
}

Authentication Headers

API Key Request

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://vote.ens.domains/api/v1/proposals

JWT Request

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  https://vote.ens.domains/api/v1/proposals

Security Best Practices

  1. Never expose API keys in client-side code or public repositories
  2. Use environment variables to store sensitive credentials
  3. Rotate keys regularly for production environments
  4. Use HTTPS for all API requests
  5. Monitor API usage in provider dashboards
  6. Set appropriate permissions based on the principle of least privilege

Environment Variables

JWT Secret

JWT_SECRET=your-secret-key-minimum-32-characters
Required: YES
Security: CRITICAL - Must be strong and unique per environment

Gas Sponsor Private Key

GAS_SPONSOR_PK=your-private-key-hex-string
Required: For gasless transactions
Security: CRITICAL - Never expose in client code

Database URLs

READ_WRITE_WEB2_DATABASE_URL_PROD=postgres://...
READ_ONLY_WEB3_DATABASE_URL_PROD=postgres://...
Required: YES
Usage: User data and blockchain indexed data

Error Responses

Missing Authentication

{
  "error": "Missing or invalid bearer token",
  "status": 401
}

Disabled User

{
  "error": "User account is disabled",
  "status": 401
}

Expired JWT

{
  "error": "JWT token has expired",
  "status": 401
}

Insufficient Permissions

{
  "error": "Unauthorized to perform action on this address",
  "status": 401
}

Testing Authentication

Test API Key

curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://vote.ens.domains/api/v1/proposals?limit=1

Test JWT

curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  https://vote.ens.domains/api/v1/proposals?limit=1
Successful authentication returns a 200 status with proposal data. Failed authentication returns 401 with an error message.

Build docs developers (and LLMs) love