Overview

Agentik Vault uses Solana wallet signature authentication instead of traditional username/password. This provides cryptographic proof of wallet ownership without exposing private keys.
Authentication flow: Nonce → Wallet Signature → JWT Token

How It Works

Step-by-Step Guide

1. Connect Wallet (Frontend)

The frontend uses @solana/react-hooks with auto-discovery:
import { useWalletConnection } from "@solana/react-hooks";

function ConnectButton() {
  const { connectors, connect, wallet, status } = useWalletConnection();
  
  const handleConnect = async (connector) => {
    await connect(connector.id);
  };
  
  if (status === "connected") {
    return <div>Connected: {wallet.account.address}</div>;
  }
  
  return (
    <div>
      {connectors.map((connector) => (
        <button key={connector.id} onClick={() => handleConnect(connector)}>
          Connect {connector.name}
        </button>
      ))}
    </div>
  );
}

2. Request Nonce

Once wallet is connected, request a challenge nonce:
const walletAddress = wallet.account.address.toString();
const domain = window.location.host;

const response = await fetch("/api/auth/nonce", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ wallet_address: walletAddress, domain }),
});

const { nonce, message } = await response.json();
// message format: "Sign this message to authenticate with Agentik Vault\nNonce: abc123\nDomain: agentikvault.com"

3. Sign Message

Request wallet to sign the message:
const messageBytes = new TextEncoder().encode(message);
const signature = await wallet.signMessage(messageBytes);
const signatureBase58 = bs58.encode(signature);
Make sure to encode the message as UTF-8 bytes before signing. The signature must be base58-encoded for transmission.

4. Verify and Get JWT

Send the signature to backend for verification:
const loginResponse = await fetch("/api/auth/login", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    wallet_address: walletAddress,
    signature_base58: signatureBase58,
    nonce: nonce,
  }),
});

const { token, user_id, wallet_address } = await loginResponse.json();

// Store JWT in localStorage
localStorage.setItem("agentik.auth", JSON.stringify({
  token,
  walletAddress: wallet_address,
  userId: user_id,
}));

5. Use JWT for API Calls

Include JWT in Authorization header for authenticated requests:
const token = JSON.parse(localStorage.getItem("agentik.auth")).token;

const response = await fetch("/api/subscription/status", {
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
});

Backend Verification

The backend verifies signatures using tweetnacl:
import nacl from "tweetnacl";
import bs58 from "bs58";

function verifySignature(message: string, signature: string, publicKey: string): boolean {
  const messageBytes = new TextEncoder().encode(message);
  const signatureBytes = bs58.decode(signature);
  const publicKeyBytes = bs58.decode(publicKey);
  
  return nacl.sign.detached.verify(messageBytes, signatureBytes, publicKeyBytes);
}
Signature verification is deterministic and cryptographically secure. No password storage required.

JWT Token Structure

The JWT token contains:
{
  "userId": "uuid-v4",
  "walletAddress": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "iat": 1705123456,
  "exp": 1705209856
}
  • userId: Database user ID (UUID)
  • walletAddress: Solana wallet public key
  • iat: Issued at (Unix timestamp)
  • exp: Expires at (Unix timestamp, 24 hours from iat)

Security Best Practices

Never expose private keys

Signatures are created by the wallet extension. Your private keys never leave the browser extension’s secure storage.

Nonce replay protection

Each nonce is single-use and expires after 5 minutes. Backend tracks used nonces in database.

JWT expiration

Tokens expire after 24 hours. Users must re-authenticate by signing a new message.

Domain binding

Message includes domain to prevent signature reuse across different sites.

Troubleshooting

  • Check wallet is unlocked
  • Ensure you’re on the correct Solana network (mainnet-beta)
  • Try disconnecting and reconnecting wallet
  • Check browser console for errors
  • Token may be expired (check exp claim)
  • Token may be invalid (corrupted localStorage)
  • Clear localStorage and re-authenticate
  • Message format must match exactly (check encoding)
  • Signature must be base58-encoded
  • Wallet address must match signer’s public key

Next Steps

Subscription Management

Learn how to subscribe to a tier and manage your subscription