Kevo Docs

Admin API

Manage users, sessions, and delegations from your backend using the KevoAdmin class from @kevo-ws/sdk/server — or via raw HTTP with your project secret key.

Admin operations must only run on your backend server. Never expose your secret key (sk_…) in client-side code, browser bundles, or mobile apps.

Setup

bash
npm install @kevo-ws/sdk
typescript
import { KevoAdmin } from '@kevo-ws/sdk/server'

const admin = new KevoAdmin({
  secretKey: process.env.KEVO_SECRET_KEY!, // sk_...
  // apiUrl: 'https://api.kevo.ws'        // optional, defaults to production
  evmRpcUrl: 'https://mainnet.base.org',   // optional — used by sendTransaction
  solanaRpcUrl: 'https://api.mainnet-beta.solana.com', // optional — used by sendSolanaTransaction
})

KevoAdmin is a plain Node.js class — no browser globals, no DOM, no React. It works in any server environment: Express, Fastify, Next.js API routes, Edge Functions, background workers, cron jobs.

Authentication

Your secret key is found in the Kevo Portal under Settings → Secret Key. The SDK automatically sends it as Authorization: Bearer sk_… on every request.

If a request fails, KevoAdmin throws a KevoAdminError with.status (HTTP code) and .message (human-readable reason).

typescript
import { KevoAdmin, KevoAdminError } from '@kevo-ws/sdk/server'

try {
  const { user } = await admin.users.get('user-uuid')
} catch (err) {
  if (err instanceof KevoAdminError) {
    console.error(err.status, err.message) // e.g. 404, "User not found"
  }
}
StatusMeaning
401Missing, malformed, or invalid secret key
403Delegation policy violation (sign methods)
404User or delegation not found in your project
422Delegation exists but is missing the required wallet capability
502 / 504Internal signing service unavailable or timed out

Users

List users

GET/v1/admin/usersSecret key

Paginated list of all users in your project.

typescript
const { users, limit, offset } = await admin.users.list({
  limit: 50,   // max 100
  offset: 0,
})

// users: Array<{
//   id: string
//   did: string
//   createdAt: string
//   authMethods: { type: string; identifier: string }[]
//   evmAddress: string | null
// }>

Get user detail

GET/v1/admin/users/:userIdSecret key

Full detail: auth methods, EVM wallet, Solana wallet, delegation status.

typescript
const { user } = await admin.users.get('a1b2c3d4-...')

// user: {
//   id, did, createdAt,
//   authMethods: [{ type: 'email', identifier: '[email protected]' }],
//   evmWallet:    { address: '0x...', createdAt: '...' } | null
//   solanaWallet: { address: '8vCy...', createdAt: '...' } | null
//   delegation:   AdminDelegation | null
// }
Encrypted wallet material is never included in any response. Only metadata and policy fields are returned.

Revoke all sessions

POST/v1/admin/users/:userId/revoke-sessionsSecret key

Force sign-out a user by invalidating all active sessions.

typescript
await admin.users.revokeSessions('a1b2c3d4-...')
// throws KevoAdminError(404) if user not found

Delegations

A delegation lets your backend sign transactions on behalf of a user without them being online. The user grants it from the client (see Delegations doc), then your backend calls the sign methods below.

List active delegations

GET/v1/admin/delegationsSecret key

All active delegations in your project.

typescript
const { delegations } = await admin.delegations.list()

// delegations: Array<{
//   id, userId, walletId, solanaWalletId,
//   policies: DelegationPolicies | null,
//   txCount: number,
//   active: boolean,
//   createdAt: string,
//   revokedAt: string | null
// }>

Sign EVM hash (delegated)

POST/v1/admin/delegations/:userId/sign-evmSecret key

Sign a 32-byte keccak256 hash on behalf of the user.

typescript
const result = await admin.delegations.signEvm('user-uuid', {
  hash: 'a3f1e0d2b9c4...64hexchars', // 64 hex chars, no 0x prefix
  chainId: 8453,                      // optional — enforced against policy
  to: '0xUniswapRouter...',           // optional — enforced against allowedContracts
  value: '0',                         // optional — wei decimal, enforced against maxAmountWei
})

// result: { r: '0x...', s: '0x...', v: 28, signature: '0x...' }
// signature is 65 bytes: r(32) || s(32) || v(1)

Provide chainId, to, and value whenever your delegation has matching policies — the API enforces them before reaching the signing service, so omitting them would bypass policy checks.

Sign Solana transaction (delegated)

POST/v1/admin/delegations/:userId/sign-solanaSecret key

Sign a Solana transaction message on behalf of the user.

typescript
import { Transaction } from '@solana/web3.js'

// Build your transaction, then serialize the message:
const tx = new Transaction().add(/* your instruction */)
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash
tx.feePayer = new PublicKey(userSolanaAddress)

const messageBase64 = Buffer.from(tx.serializeMessage()).toString('base64')

const { signature } = await admin.delegations.signSolana('user-uuid', {
  message: messageBase64,
})

// Re-attach the signature and broadcast:
tx.addSignature(
  new PublicKey(userSolanaAddress),
  Buffer.from(signature, 'base64'),
)
await connection.sendRawTransaction(tx.serialize())

Send EVM Transaction (sign + broadcast)

sendTransaction() is a high-level helper that signs and broadcasts an EVM transaction in one call. It automatically fills nonce, gasLimit, and maxFeePerGas from the RPC if not provided. Returns the transaction hash.

POST/v1/admin/delegations/:userId/sign-evm-txSecret key

Sign a full EVM transaction (called internally by sendTransaction).

typescript
// Simplest usage — just to, value, chainId:
const txHash = await admin.delegations.sendTransaction(
  'user-uuid',
  {
    to: '0xRecipient...',
    value: '0x2386f26fc10000', // 0.01 ETH in hex
    chainId: 8453,             // Base
  },
)
console.log('TX hash:', txHash)

// With calldata (e.g. ERC-20 transfer):
const txHash2 = await admin.delegations.sendTransaction(
  'user-uuid',
  {
    to: '0xUSDC...',
    data: '0xa9059cbb' + '...', // transfer(address,uint256)
    value: '0x0',
    chainId: 8453,
  },
)

// Override gas/nonce if needed:
const txHash3 = await admin.delegations.sendTransaction(
  'user-uuid',
  {
    to: '0x...',
    data: '0x...',
    chainId: 8453,
    nonce: 42,
    gasLimit: '0x30000',
    maxFeePerGas: '0x59682F00',
    maxPriorityFeePerGas: '0x59682F00',
  },
  'https://custom-rpc.example.com', // optional per-call RPC override
)
The RPC URL is resolved in this order: explicit rpcUrl argument → chains[tx.chainId] from KevoAdminConfig evmRpcUrl from KevoAdminConfig. If none is set, the call throws immediately.

Send Solana Transaction (sign + broadcast)

sendSolanaTransaction() signs and broadcasts a Solana transaction in one call. Pass the unsigned message bytes (or base64); the SDK handles signature assembly and RPC broadcast. Returns the transaction signature (base58).

typescript
import { Transaction, PublicKey, SystemProgram } from '@solana/web3.js'

const tx = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(userSolAddress),
    toPubkey: new PublicKey('Recipient...'),
    lamports: 1_000_000, // 0.001 SOL
  }),
)
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash
tx.feePayer = new PublicKey(userSolAddress)

const sig = await admin.delegations.sendSolanaTransaction(
  'user-uuid',
  { message: tx.serializeMessage() }, // Uint8Array or base64 string
  // optional per-call RPC override:
  // 'https://api.mainnet-beta.solana.com'
)
console.log('Solana tx sig:', sig)

Full example

A Next.js API route that sends a USDC transfer on behalf of a user with an active delegation. Using sendTransaction, no manual hash signing or gas estimation required:

typescript
// app/api/transfer/route.ts
import { KevoAdmin, KevoAdminError } from '@kevo-ws/sdk/server'
import { encodeFunctionCall } from '@kevo-ws/sdk/helpers'

const admin = new KevoAdmin({
  secretKey: process.env.KEVO_SECRET_KEY!,
  evmRpcUrl: 'https://mainnet.base.org',
})

const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base

export async function POST(req: Request) {
  const { userId, recipient, amount } = await req.json()

  // Build TransactionRequest using the SDK helper
  const tx = encodeFunctionCall({
    to: USDC,
    functionSignature: 'transfer(address,uint256)',
    params: [
      { type: 'address', value: recipient },
      { type: 'uint256', value: BigInt(amount) },
    ],
    chainId: 8453,
  })

  try {
    const txHash = await admin.delegations.sendTransaction(userId, {
      to: tx.to,
      data: tx.data,
      value: '0x0',
      chainId: tx.chainId,
    })
    return Response.json({ txHash })
  } catch (err) {
    if (err instanceof KevoAdminError) {
      return Response.json({ error: err.message }, { status: err.status })
    }
    throw err
  }
}

Raw HTTP reference

If you're not using Node.js or prefer bare HTTP, all endpoints accept Authorization: Bearer sk_… and return JSON.

http
# List users
GET /v1/admin/users?limit=50&offset=0
Authorization: Bearer sk_...

# Get user
GET /v1/admin/users/:userId
Authorization: Bearer sk_...

# Revoke sessions
POST /v1/admin/users/:userId/revoke-sessions
Authorization: Bearer sk_...

# List delegations
GET /v1/admin/delegations
Authorization: Bearer sk_...

# Sign EVM hash
POST /v1/admin/delegations/:userId/sign-evm
Authorization: Bearer sk_...
Content-Type: application/json
{ "hash": "64hexchars", "chainId": 8453, "to": "0x...", "value": "0" }

# Sign Solana message
POST /v1/admin/delegations/:userId/sign-solana
Authorization: Bearer sk_...
Content-Type: application/json
{ "message": "base64EncodedTxMessage..." }

# Sign full EVM transaction (returns rawTransaction for broadcast)
POST /v1/admin/delegations/:userId/sign-evm-tx
Authorization: Bearer sk_...
Content-Type: application/json
{
  "chainId": 8453,
  "to": "0x...",
  "data": "0xa9059cbb...",
  "value": "0x0",
  "nonce": 42,
  "gasLimit": "0x30000",
  "maxFeePerGas": "0x59682F00",
  "maxPriorityFeePerGas": "0x59682F00"
}