Kevo Docs

Solana Wallet Authentication

Let users sign in with Phantom, Solflare, or Backpack using an Ed25519 challenge-sign flow.

How it works

Solana wallet auth uses the same challenge-sign pattern as EVM, but with Ed25519:

  1. Kevo generates a nonce for the Solana address.
  2. The SDK signs the challenge with the wallet's private key via signMessage().
  3. Kevo verifies the 64-byte Ed25519 signature and issues a session.

Pre-built Modal

If the project has solana in enabledChains, the modal shows Solana wallet options automatically:

Phantom

window.phantom?.solana

Solflare

window.solflare

Backpack

window.backpack?.solana
When both evm and solana chains are enabled, the modal shows all wallet types with EVM / SOL chain badges.

Programmatic Sign-in

typescript
import { useKevo } from '@kevo-ws/sdk/react'

function ConnectPhantom() {
  const { client } = useKevo()

  const connect = async () => {
    const phantom = window.phantom?.solana
    if (!phantom) {
      alert('Please install Phantom')
      return
    }
    await client.loginWithSolanaWallet({ provider: phantom })
  }

  return <button onClick={connect}>Connect Phantom</button>
}

Provider Quirks

Solflare, connect() returns void

Unlike Phantom, connect() on Solflare returns void. The SDK reads the public key from provider.publicKey after connecting.

Phantom, requires 'utf8' hint

Phantom's signMessage() requires a second 'utf8' argument or it throws an internal -32603 "Unexpected error". The SDK branches onprovider.isPhantom and passes the hint only when needed.

API Flow

1. Get nonce

http
GET /v1/auth/sol-wallet/nonce
  ?address=8vCy...XYZ
  &projectId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# Response:
{
  "message": "Sign in to Kevo\n\nWallet: 8vCy...XYZ\nNonce: abc123\nIssued At: ...",
  "nonce": "abc123"
}

2. Verify signature

http
POST /v1/auth/sol-wallet/verify
Content-Type: application/json

{
  "address": "8vCy...XYZ",
  "message": "Sign in to Kevo\n\n...",
  "signature": "128-char hex string (64-byte Ed25519 sig)",
  "projectId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

# Response:
{
  "accessToken": "eyJ...",
  "expiresIn": 900,
  "address": "8vCy...XYZ"
}
# Plus: Set-Cookie: kevo_refresh_token=...

SolanaProvider Interface

typescript
interface SolanaProvider {
  isPhantom?: boolean
  isSolflare?: boolean
  isBackpack?: boolean
  publicKey?: { toBase58(): string }
  connect(opts?: { onlyIfTrusted?: boolean }): Promise<void | { publicKey: { toBase58(): string } }>
  disconnect(): Promise<void>
  signMessage(message: Uint8Array): Promise<{ signature: Uint8Array } | Uint8Array>
}