Skip to main content

Overview

The Kuest API supports multiple authentication methods depending on the endpoint and use case:
  1. Session-based authentication - For web application requests
  2. HMAC signature authentication - For CLOB (Central Limit Order Book) API requests
  3. Wallet authentication (SIWE) - Sign-In with Ethereum for user accounts

Session Authentication

Most authenticated endpoints use session cookies managed by Better Auth. When users connect their wallet, a session is created automatically.

Session Structure

Sessions are stored in the database and validated on each request:
// Session fields (from src/lib/auth.ts:357-371)
session: {
  modelName: 'sessions',
  cookieCache: {
    enabled: true,
    maxAge: 5 * 60, // 5 minutes
  },
  fields: {
    userId: 'user_id',
    token: 'token',
    expiresAt: 'expires_at',
    ipAddress: 'ip_address',
    userAgent: 'user_agent',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
  },
}
```typescript

### Getting Current User

Authenticated endpoints check for a valid session:

<CodeGroup>

```typescript Example: Notifications Endpoint
// src/app/api/notifications/route.ts
import { UserRepository } from '@/lib/db/queries/user'

export async function GET() {
  const user = await UserRepository.getCurrentUser()
  
  if (!user) {
    return NextResponse.json(
      { error: 'Unauthenticated.' },
      { status: 401 }
    )
  }
  
  // Proceed with authenticated request
  const { data: notifications } = await NotificationRepository.getByUserId(user.id)
  return NextResponse.json(notifications)
}
```typescript

```typescript Example: Events Endpoint
// src/app/api/events/route.ts
export async function GET(request: Request) {
  // User is optional - returns public data if not authenticated
  const user = await UserRepository.getCurrentUser()
  const userId = user?.id
  
  const { data: events } = await EventRepository.listEvents({
    userId, // Used for bookmarks and personalization
    tag,
    search,
    status,
  })
  
  return NextResponse.json(events)
}
```typescript

</CodeGroup>

## HMAC Signature Authentication

The CLOB API requires HMAC-SHA256 signatures for authenticated requests. This is used for trading operations, order management, and position queries.

### HMAC Signature Process

<Steps>
  <Step title="Get Trading Credentials">
    Users must first obtain API credentials from [auth.kuest.com](https://auth.kuest.com):
    - `KUEST_API_KEY` - Public API key identifier
    - `KUEST_API_SECRET` - Secret key (base64 encoded)
    - `KUEST_PASSPHRASE` - Additional passphrase
  </Step>
  
  <Step title="Build Signature String">
    Concatenate timestamp, HTTP method, path, and body (if present)
  </Step>
  
  <Step title="Sign with HMAC-SHA256">
    Use the base64-decoded secret to sign the message
  </Step>
  
  <Step title="Encode as URL-safe Base64">
    Replace `+` with `-` and `/` with `_`
  </Step>
</Steps>

### Implementation

The HMAC signing logic is in `src/lib/hmac.ts`:

<CodeGroup>

```typescript HMAC Signing Function
// src/lib/hmac.ts
import crypto from 'node:crypto'
import { Buffer } from 'node:buffer'

export function buildClobHmacSignature(
  secret: string,
  timestamp: number,
  method: string,
  requestPath: string,
  body?: string
): string {
  // For GET requests, strip query parameters from path
  const normalizedPath = method === 'GET'
    ? requestPath.split('?')[0]
    : requestPath
  
  // Build message: timestamp + method + path + body
  let message = timestamp + method + normalizedPath
  if (body !== undefined) {
    message += body
  }
  
  // Decode base64 secret
  const base64Secret = Buffer.from(secret, 'base64')
  
  // Create HMAC-SHA256 signature
  const hmac = crypto.createHmac('sha256', base64Secret)
  const sig = hmac.update(message).digest('base64')
  
  // URL-safe base64 encoding
  return sig.replace(/\+/g, '-').replace(/\//g, '_')
}
```typescript

```typescript Usage Example
// src/app/api/open-orders/route.ts
import { buildClobHmacSignature } from '@/lib/hmac'

const CLOB_URL = process.env.CLOB_URL

async function fetchClobOpenOrders({
  auth,
  userAddress,
  makerAddress,
  market,
}: {
  auth: { key: string, secret: string, passphrase: string }
  userAddress: string
  makerAddress?: string
  market?: string
}) {
  // Build query parameters
  const params = new URLSearchParams()
  if (makerAddress) params.set('maker', makerAddress)
  if (market) params.set('market', market)
  
  const path = `/data/orders?${params.toString()}`
  
  // Generate timestamp (Unix seconds)
  const timestamp = Math.floor(Date.now() / 1000)
  
  // Build HMAC signature
  const signature = buildClobHmacSignature(
    auth.secret,
    timestamp,
    'GET',
    path
  )
  
  // Make authenticated request
  const response = await fetch(`${CLOB_URL}${path}`, {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
      'KUEST_ADDRESS': userAddress,
      'KUEST_API_KEY': auth.key,
      'KUEST_PASSPHRASE': auth.passphrase,
      'KUEST_TIMESTAMP': timestamp.toString(),
      'KUEST_SIGNATURE': signature,
    },
  })
  
  return await response.json()
}
```typescript

</CodeGroup>

### Required Headers

All CLOB API requests must include these headers:

| Header | Description | Example |
|--------|-------------|----------|
| `KUEST_ADDRESS` | User's EVM wallet address | `0x1234...5678` |
| `KUEST_API_KEY` | Public API key | `abc123...` |
| `KUEST_PASSPHRASE` | API passphrase | `my-passphrase` |
| `KUEST_TIMESTAMP` | Current Unix timestamp (seconds) | `1709481600` |
| `KUEST_SIGNATURE` | HMAC-SHA256 signature | `abcd1234_efgh5678...` |

### Signature Examples

<CodeGroup>

```typescript GET Request
// GET /data/orders?maker=0x1234
const timestamp = 1709481600
const method = 'GET'
const path = '/data/orders' // Query params stripped for GET
const body = undefined

const message = `${timestamp}${method}${path}`
// "1709481600GET/data/orders"

const signature = buildClobHmacSignature(secret, timestamp, method, path)
```typescript

```typescript POST Request
// POST /orders with JSON body
const timestamp = 1709481600
const method = 'POST'
const path = '/orders'
const body = JSON.stringify({
  market: '0xabc...',
  price: 0.75,
  size: 100
})

const message = `${timestamp}${method}${path}${body}`
// "1709481600POST/orders{\"market\":\"0xabc...\",...}"

const signature = buildClobHmacSignature(secret, timestamp, method, path, body)
```typescript

</CodeGroup>

## Wallet Authentication (SIWE)

Users authenticate using Sign-In with Ethereum (SIWE), implemented via Better Auth and Reown AppKit.

### Configuration

Wallet authentication is configured in `src/lib/auth.ts`:

```typescript
// src/lib/auth.ts:256-290
import { siwe } from 'better-auth/plugins'
import { getChainIdFromMessage } from '@reown/appkit-siwe'
import { createPublicClient, http } from 'viem'

export const auth = betterAuth({
  // ... other config
  plugins: [
    siwe({
      schema: {
        walletAddress: {
          modelName: 'wallets',
          fields: {
            userId: 'user_id',
            address: 'address',
            chainId: 'chain_id',
            isPrimary: 'is_primary',
            createdAt: 'created_at',
          },
        },
      },
      domain: SIWE_DOMAIN,           // e.g., 'kuest.com'
      emailDomainName: SIWE_EMAIL_DOMAIN, // e.g., 'kuest.com'
      anonymous: true,
      getNonce: async () => generateRandomString(32),
      verifyMessage: async ({ message, signature, address }) => {
        const chainId = getChainIdFromMessage(message)
        
        // Use Reown WalletConnect RPC
        const publicClient = createPublicClient({
          transport: http(
            `https://rpc.walletconnect.org/v1/?chainId=${chainId}&projectId=${projectId}`
          ),
        })
        
        return await publicClient.verifyMessage({
          message,
          address: address as `0x${string}`,
          signature: signature as `0x${string}`,
        })
      },
    }),
  ],
})
```typescript

### SIWE Flow

<Steps>
  <Step title="User connects wallet">
    User clicks "Connect Wallet" and selects their Web3 wallet (MetaMask, WalletConnect, etc.)
  </Step>
  
  <Step title="Request nonce">
    Frontend requests a random nonce from the server
  </Step>
  
  <Step title="Sign message">
    User signs a SIWE message containing:
    - Domain (e.g., `kuest.com`)
    - User's wallet address
    - Chain ID (e.g., Polygon)
    - Nonce (prevents replay attacks)
    - Timestamp
  </Step>
  
  <Step title="Verify signature">
    Server verifies the signature using a public RPC node
  </Step>
  
  <Step title="Create session">
    If valid, create a user account (if new) and start a session
  </Step>
</Steps>

### User Model

Authenticated users have the following structure:

```typescript
// src/lib/auth.ts:311-352
user: {
  modelName: 'users',
  fields: {
    name: 'address',        // Primary wallet address
    email: 'email',
    emailVerified: 'email_verified',
    image: 'image',
    createdAt: 'created_at',
    updatedAt: 'updated_at',
  },
  additionalFields: {
    address: { type: 'string' },
    username: { type: 'string' },
    settings: { type: 'json' },
    proxy_wallet_address: { type: 'string' },
    proxy_wallet_signature: { type: 'string' },
    proxy_wallet_status: { type: 'string' },
    affiliate_code: { type: 'string' },
  },
}
```typescript

## Trading Auth Storage

API credentials are stored encrypted in the user's settings. The system uses AES-256-GCM encryption.

### Storing Credentials

```typescript
// src/lib/trading-auth/server.ts
import { encryptSecret } from '@/lib/encryption'

export async function saveUserTradingAuthCredentials(
  userId: string,
  payload: {
    relayer?: { key: string, secret: string, passphrase: string }
    clob?: { key: string, secret: string, passphrase: string }
  }
) {
  // Encrypt each credential
  const tradingAuth = {
    relayer: payload.relayer ? {
      key: encryptSecret(payload.relayer.key),
      secret: encryptSecret(payload.relayer.secret),
      passphrase: encryptSecret(payload.relayer.passphrase),
      updatedAt: new Date().toISOString(),
    } : undefined,
    clob: payload.clob ? {
      key: encryptSecret(payload.clob.key),
      secret: encryptSecret(payload.clob.secret),
      passphrase: encryptSecret(payload.clob.passphrase),
      updatedAt: new Date().toISOString(),
    } : undefined,
  }
  
  // Store in user.settings.tradingAuth
  await db.update(users)
    .set({ settings: { tradingAuth } })
    .where(eq(users.id, userId))
}
```typescript

### Retrieving Credentials

```typescript
export async function getUserTradingAuthSecrets(
  userId: string
): Promise<TradingAuthSecrets | null> {
  const [row] = await db
    .select({ settings: users.settings })
    .from(users)
    .where(eq(users.id, userId))
    .limit(1)
  
  const tradingAuth = row?.settings?.tradingAuth
  if (!tradingAuth) return null
  
  // Decrypt credentials
  return {
    relayer: decodeEntry(tradingAuth.relayer),
    clob: decodeEntry(tradingAuth.clob),
  }
}

function decodeEntry(entry?: TradingAuthSecretEntry) {
  if (!entry) return undefined
  
  return {
    key: decryptSecret(entry.key),
    secret: decryptSecret(entry.secret),
    passphrase: decryptSecret(entry.passphrase),
  }
}
```typescript

## Two-Factor Authentication

Users can enable TOTP-based 2FA for additional security.

### 2FA Configuration

```typescript
// src/lib/auth.ts:292-309
import { twoFactor } from 'better-auth/plugins'

export const auth = betterAuth({
  plugins: [
    twoFactor({
      skipVerificationOnEnable: false,
      schema: {
        user: {
          fields: {
            twoFactorEnabled: 'two_factor_enabled',
          },
        },
        twoFactor: {
          modelName: 'two_factors',
          fields: {
            secret: 'secret',
            backupCodes: 'backup_codes',
            userId: 'user_id',
          },
        },
      },
    }),
  ],
})
```typescript

When 2FA is enabled, users must verify their TOTP code after wallet signature verification.

## Security Best Practices

<Warning>
  Never expose your API secret or passphrase in client-side code. Always make authenticated requests from your backend.
</Warning>

### Credential Security

1. **Rotate credentials regularly** - Generate new API keys periodically
2. **Use environment variables** - Never hardcode credentials
3. **Encrypt at rest** - User credentials are encrypted using `BETTER_AUTH_SECRET`
4. **HTTPS only** - Always use secure connections for API requests

### Timestamp Validation

HMAC signatures include timestamps to prevent replay attacks:

```typescript
const timestamp = Math.floor(Date.now() / 1000)

// Timestamp must be within acceptable window (typically ±30 seconds)
if (Math.abs(currentTime - requestTime) > 30) {
  throw new Error('Request timestamp too old or in future')
}
```typescript

### L2 Auth Context

The system uses "L2 Auth Context" cookies to validate trading sessions across devices:

```typescript
// When credentials are saved, create a unique context ID
const l2AuthContextId = createL2AuthContextId() // Random 32-byte hex
const contextHash = hashL2AuthContextId(l2AuthContextId)

// Store hash in user.settings.tradingAuth.l2Contexts
// Set cookie with the actual ID (not the hash)
// Max 5 contexts per user
```typescript

This ensures credentials are only accessible from devices where they were originally saved.

## Next Steps

<CardGroup cols={2}>
  <Card title="List Markets" icon="list" href="/api/markets/list-markets">
    Query available prediction markets
  </Card>
  <Card title="Place Orders" icon="cart-shopping" href="/api/trading/place-order">
    Submit trades using HMAC authentication
  </Card>
  <Card title="Get Positions" icon="wallet" href="/api/positions/current-positions">
    Check your current holdings
  </Card>
  <Card title="Integration Guide" icon="code" href="/integration/overview">
    Build a trading bot with the API
  </Card>
</CardGroup>

Build docs developers (and LLMs) love