@kevo-ws/sdk
v0.2.5
Published
Embedded wallet infrastructure for web applications. Drop in authentication (email OTP, Google, X, Apple), automatic EVM and Solana wallet creation, and signing — all without users ever managing seed phrases.
Readme
@kevo-ws/sdk
Embedded wallet infrastructure for web applications. Drop in authentication (email OTP, Google, X, Apple), automatic EVM and Solana wallet creation, and signing — all without users ever managing seed phrases.
Table of contents
- @kevo-ws/sdk
Installation
npm install @kevo-ws/sdkReact and React DOM are optional peer dependencies — only required if you use the React bindings.
Quick start (React)
// main.tsx
import { KevoProvider } from '@kevo-ws/sdk/react'
root.render(
<KevoProvider config={{
publishableKey: 'pk_live_...',
evmRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
solanaRpcUrl: 'https://api.mainnet-beta.solana.com',
}}>
<App />
</KevoProvider>
)// App.tsx
import { KevoModal } from '@kevo-ws/sdk/react'
import { useKevo, useWallets } from '@kevo-ws/sdk/react'
export function App() {
const { isAuthenticated, isLoading } = useKevo()
const { evmWallet, solanaWallet } = useWallets()
if (isLoading) return <p>Loading…</p>
if (!isAuthenticated) return <KevoModal />
return (
<div>
<p>EVM: {evmWallet?.address}</p>
<p>Solana: {solanaWallet?.address}</p>
</div>
)
}<KevoModal> handles the full auth flow (email OTP + social login) with zero configuration. Style it via the portal's UI Config panel.
Authentication
Email OTP
import { useKevo } from '@kevo-ws/sdk/react'
function LoginForm() {
const { sendEmailOtp, verifyEmailOtp } = useKevo()
const [step, setStep] = useState<'email' | 'otp'>('email')
const [email, setEmail] = useState('')
const handleSendOtp = async () => {
await sendEmailOtp(email)
setStep('otp')
}
const handleVerify = async (code: string) => {
await verifyEmailOtp(email, code)
// isAuthenticated becomes true, wallet is created automatically
}
// ...
}Social login
const { loginWithGoogle, loginWithX, loginWithApple } = useKevo()
<button onClick={loginWithGoogle}>Continue with Google</button>
<button onClick={loginWithX}>Continue with X</button>
<button onClick={loginWithApple}>Continue with Apple</button>Social auth opens a popup/redirect to the OAuth provider and returns with an active session.
Session state
const {
isAuthenticated, // boolean
isLoading, // true during initial session restore
session, // { userId, did, accessToken, expiresAt, projectId } | null
logout,
} = useKevo()Wallets
Wallets are created automatically on first sign-in. Each user gets one EVM wallet and one Solana wallet depending on which chains are enabled in your project.
import { useWallet, useSolanaWallet, useWallets } from '@kevo-ws/sdk/react'
// Single-chain
const { wallet } = useWallet() // { id, address, createdAt } | null
const { solanaWallet } = useSolanaWallet()
// Both chains at once
const { evmWallet, solanaWallet, enabledChains } = useWallets()Wallet addresses persist across sessions — the same user always gets the same address.
Signing
Sign a message
import { useSignMessage } from '@kevo-ws/sdk/react'
const { signMessage, isLoading, error } = useSignMessage()
// Auto-routes to EVM or Solana based on your project's enabled chains
const signature = await signMessage('Hello Kevo')
// Multi-chain project — specify explicitly
const sig = await signMessage('Hello', { chain: 'solana' })Sign typed data (EIP-712)
import { useSignTypedData } from '@kevo-ws/sdk/react'
const { signTypedData } = useSignTypedData()
const signature = await signTypedData({
domain: { name: 'MyApp', version: '1', chainId: 1 },
types: { Order: [{ name: 'amount', type: 'uint256' }] },
primaryType: 'Order',
message: { amount: 1000 },
})Sign a Solana transaction
import { useSignSolanaTransaction } from '@kevo-ws/sdk/react'
import { solTransfer, getRecentBlockhash } from '@kevo-ws/sdk'
const { signSolanaTransaction, sendSolanaTransaction } = useSignSolanaTransaction()
const { solanaWallet } = useSolanaWallet()
const blockhash = await getRecentBlockhash('https://api.mainnet-beta.solana.com')
const txBytes = solTransfer({
from: solanaWallet!.address,
to: 'RecipientBase58...',
lamports: 10_000_000, // 0.01 SOL
recentBlockhash: blockhash,
})
// Sign only (returns hex signature)
const sigHex = await signSolanaTransaction(txBytes)
// Sign + broadcast (returns base58 tx signature)
const txSig = await sendSolanaTransaction(txBytes, 'https://api.mainnet-beta.solana.com')Signing confirmation UI
By default a confirmation modal appears before each signing operation, showing the user what they are about to sign. Mount <KevoSigningConfirmation> anywhere in your tree to render it:
import { KevoSigningConfirmation } from '@kevo-ws/sdk/react'
// Place it at the root level so it can render as an overlay
<KevoSigningConfirmation />To skip confirmations entirely (e.g. for automated flows), set hideSigningConfirmations: true in your project's UI Config in the portal.
Sending transactions
EVM
import { useSignTransaction } from '@kevo-ws/sdk/react'
import { erc20Transfer } from '@kevo-ws/sdk'
const { sendTransaction } = useSignTransaction()
// Native ETH transfer
await sendTransaction(
{ to: '0xRecipient...', value: 100_000_000_000_000_000n, chainId: 1 },
'https://mainnet.infura.io/v3/YOUR_KEY'
)
// ERC-20 transfer — build the calldata with a helper
const tx = erc20Transfer({
token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
to: '0xRecipient...',
amount: 1_000_000n, // 1 USDC (6 decimals)
chainId: 1,
})
await sendTransaction(tx, 'https://mainnet.infura.io/v3/YOUR_KEY')Solana
const { sendSolanaTransaction } = useSignTransaction()
await sendSolanaTransaction(
txBytes,
'https://api.mainnet-beta.solana.com',
{ chain: 'solana' }
)Default RPC URL (single-chain)
Set a default RPC URL in the provider config so you never have to pass it per-call:
<KevoProvider config={{
publishableKey: 'pk_live_...',
evmRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
solanaRpcUrl: 'https://api.mainnet-beta.solana.com',
}}>Multi-chain EVM
Use chains to configure multiple EVM networks at once. The SDK automatically picks the right RPC from tx.chainId — no per-call config needed.
<KevoProvider config={{
publishableKey: 'pk_live_...',
defaultChainId: 1, // which chain is active on startup
chains: {
1: 'https://eth-mainnet.infura.io/v3/YOUR_KEY',
8453: 'https://base-mainnet.infura.io/v3/YOUR_KEY',
137: 'https://polygon-mainnet.infura.io/v3/YOUR_KEY',
},
}}>Switch chain with useChain:
import { useChain } from '@kevo-ws/sdk/react'
function ChainSwitcher() {
const { activeChainId, setChain, chains } = useChain()
return (
<div>
{Object.keys(chains).map(id => (
<button
key={id}
onClick={() => setChain(Number(id))}
style={{ fontWeight: Number(id) === activeChainId ? 'bold' : 'normal' }}
>
Chain {id}
</button>
))}
</div>
)
}After setChain(8453):
useBalance()fetches from Base automaticallyuseTokenBalance('0x...')fetches from Base automaticallysendTransaction({ to, value, chainId: 8453 })routes to Base — norpcUrlargument needed
When calling sendTransaction, the RPC is resolved from tx.chainId first, so you can fire transactions on any chain regardless of which one is currently active:
const { sendTransaction } = useSignTransaction()
// These all resolve their RPC automatically from tx.chainId
await sendTransaction({ to: '0x...', chainId: 1 }) // Ethereum
await sendTransaction({ to: '0x...', chainId: 8453 }) // Base
await sendTransaction({ to: '0x...', chainId: 137 }) // PolygonReal-world example — lending protocol on multiple chains:
function LendingPage() {
const { activeChainId, setChain } = useChain()
const { balance } = useBalance()
const { sendTransaction } = useSignTransaction()
const LENDING_CONTRACT: Record<number, string> = {
1: '0xEthLendingContract...',
137: '0xPolygonLendingContract...',
}
const supply = async (amount: bigint) => {
const contract = LENDING_CONTRACT[activeChainId!]
const tx = encodeFunctionCall({
to: contract,
functionSignature: 'supply(address,uint256)',
params: [
{ type: 'address', value: usdcAddress },
{ type: 'uint256', value: amount },
],
chainId: activeChainId!,
})
await sendTransaction(tx) // RPC resolved automatically
}
return (
<>
<button onClick={() => setChain(1)}>Ethereum</button>
<button onClick={() => setChain(137)}>Polygon</button>
<p>Balance on chain {activeChainId}: {balance?.toString()} wei</p>
<button onClick={() => supply(1_000_000n)}>Supply USDC</button>
</>
)
}Balances
import { useBalance, useTokenBalance, useSolanaBalance } from '@kevo-ws/sdk/react'
// Native ETH balance (wei, auto-refreshes every 15s)
const { balance } = useBalance()
// ERC-20 token balance
const { balance } = useTokenBalance('0xA0b8...USDC')
// SOL balance (lamports)
const { balance } = useSolanaBalance()
// Custom poll interval (ms) or disable polling
const { balance, refetch } = useBalance(undefined, 60_000)
const { balance } = useBalance(undefined, 0) // no pollingAll balance hooks return { balance: bigint | null, isLoading, error, refetch }.
Key export
Users can export their raw private key. The key is displayed inside a Kevo-controlled iframe — it is never accessible from your application's JavaScript context.
import { useExportKey, useSolanaExportKey } from '@kevo-ws/sdk/react'
function ExportButton() {
const { requestExport, confirmExport } = useExportKey() // EVM
// const { requestExport, confirmExport } = useSolanaExportKey() // Solana
const handleExport = async () => {
// Step 1: sends OTP to user's email, returns masked email address
const maskedEmail = await requestExport()
const code = prompt(`Enter the OTP sent to ${maskedEmail}`)
// Step 2: verifies OTP — private key is shown inside the secure iframe
await confirmExport(code!)
}
return <button onClick={handleExport}>Export private key</button>
}Delegation (server-side signing)
Delegation lets your backend sign transactions on behalf of a user without user interaction. The user grants permission once from the frontend; your server can then sign at any time within the policies you define.
1. User grants delegation (frontend)
import { useKevo } from '@kevo-ws/sdk/react'
const { grantDelegation, revokeDelegation, getDelegation } = useKevo()
// Grant with optional policy restrictions
await grantDelegation({
include: 'evm', // 'evm' | 'solana' | 'both'
policies: {
expiresAt: '2025-12-31T23:59:59Z',
maxTxCount: 100,
allowedChainIds: [8453, 1], // Base + Ethereum
allowedContracts: ['0xRouter…'], // only these contracts
maxAmountWei: '1000000000000000000', // max 1 ETH per tx
},
})
// Check delegation status
const delegation = await getDelegation()
console.log(delegation?.active, delegation?.txCount)
// Revoke
await revokeDelegation()2. Server signs on behalf of user
import { KevoAdmin } from '@kevo-ws/sdk/server'
const admin = new KevoAdmin({
secretKey: process.env.KEVO_SECRET_KEY!,
evmRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
})
// Sign + broadcast an EVM transaction (auto-fills nonce, gas, fees)
const txHash = await admin.delegations.sendTransaction(userId, {
to: '0xRecipient...',
value: '0x2386f26fc10000', // 0.01 ETH in hex
chainId: 8453,
})
// Sign + broadcast a Solana transaction
const txSig = await admin.delegations.sendSolanaTransaction(
userId,
{ message: txMessageBytes },
'https://api.mainnet-beta.solana.com',
)Delegation policies are enforced server-side before every sign call.
Server SDK
@kevo-ws/sdk/server provides the KevoAdmin class for backend use (Node.js, Bun, Deno, Edge functions).
import { KevoAdmin } from '@kevo-ws/sdk/server'
const admin = new KevoAdmin({
secretKey: process.env.KEVO_SECRET_KEY!,
evmRpcUrl: '...', // optional default
solanaRpcUrl: '...', // optional default
})Users
// List all users (paginated)
const { users } = await admin.users.list({ limit: 50, offset: 0 })
// Get a single user with wallet + delegation info
const { user } = await admin.users.get('user-uuid')
// user.evmWallet.address, user.solanaWallet.address, user.delegation
// Force sign-out all sessions for a user
await admin.users.revokeSessions('user-uuid')Delegations
// List active delegations in your project
const { delegations } = await admin.delegations.list()
// Low-level: sign a raw 32-byte hash (EVM)
const { signature } = await admin.delegations.signEvm('user-uuid', {
hash: 'a3f1e0d2...64hexchars', // keccak256 hash, no 0x prefix
chainId: 8453,
to: '0xContract...',
value: '0',
})
// Low-level: sign a Solana transaction message
const { signature } = await admin.delegations.signSolana('user-uuid', {
message: Buffer.from(txMessage).toString('base64'),
})Webhooks
Kevo sends signed webhook events to your endpoint when users authenticate, wallets are created, transactions are submitted, and more.
Verify webhook signature
import { verifyWebhookSignature } from '@kevo-ws/sdk/server'
import type { WebhookPayload } from '@kevo-ws/sdk/server'
// Express / Fastify / any Node framework
app.post('/webhooks/kevo', (req, res) => {
const isValid = verifyWebhookSignature(
req.body, // raw body string — do NOT parse before this
req.headers['x-kevo-signature'], // 'sha256=abc123...'
process.env.KEVO_WEBHOOK_SECRET!,
)
if (!isValid) return res.status(401).send('Invalid signature')
const payload = JSON.parse(req.body) as WebhookPayload
// payload.event: 'user.created' | 'user.authenticated' | 'wallet.created' | ...
// payload.data: { userId, method, ... }
// payload.timestamp: unix ms
res.sendStatus(200)
})Available events
| Event | Fired when |
|---|---|
| user.created | New user signs up |
| user.authenticated | Any successful sign-in |
| wallet.created | EVM or Solana wallet generated |
| wallet.recovery_setup | User sets up recovery |
| wallet.recovered | Wallet recovered |
| private_key.exported | User exports their private key |
| signing.completed | A signing request completes |
| transaction.submitted | A transaction is broadcast |
Configure webhooks and get the secret from the Kevo Portal → your project → Webhooks.
Transaction helpers
Zero-dependency helpers for building transaction calldata. Import from @kevo-ws/sdk (client) or @kevo-ws/sdk/helpers.
EVM
import { erc20Transfer, erc20Approve, nativeTransfer, encodeFunctionCall } from '@kevo-ws/sdk'
// ERC-20 transfer
const tx = erc20Transfer({ token: '0xUSDC…', to: '0xRecipient…', amount: 1_000_000n, chainId: 1 })
// ERC-20 approve
const tx = erc20Approve({ token: '0xUSDC…', spender: '0xRouter…', amount: 2n ** 256n - 1n, chainId: 1 })
// Native ETH transfer
const tx = nativeTransfer({ to: '0xRecipient…', value: 100_000_000_000_000_000n, chainId: 1 })
// Generic contract call (address, uint256, bool, bytes32 params)
const tx = encodeFunctionCall({
to: '0xContract…',
functionSignature: 'stake(address,uint256)',
params: [
{ type: 'address', value: '0xToken…' },
{ type: 'uint256', value: 500_000_000n },
],
chainId: 8453,
})Solana
import {
solTransfer,
splTokenTransfer,
getAssociatedTokenAddress,
assembleSolanaTransaction,
getRecentBlockhash,
broadcastSolanaTransaction,
base58Encode, base58Decode,
} from '@kevo-ws/sdk'
// Fetch recent blockhash
const blockhash = await getRecentBlockhash('https://api.mainnet-beta.solana.com')
// Native SOL transfer
const msgBytes = solTransfer({
from: solanaWallet.address,
to: 'RecipientBase58…',
lamports: 10_000_000, // 0.01 SOL
recentBlockhash: blockhash,
})
// SPL token transfer
const ataFrom = await getAssociatedTokenAddress(solanaWallet.address, mintAddress)
const ataTo = await getAssociatedTokenAddress(recipientAddress, mintAddress)
const msgBytes = splTokenTransfer({
from: solanaWallet.address,
sourceAta: ataFrom,
destinationAta: ataTo,
mint: mintAddress,
amount: 1_000_000n, // 1 USDC (6 decimals)
recentBlockhash: blockhash,
})
// Broadcast an already-signed transaction
const txSig = await broadcastSolanaTransaction(signedTxBytes, 'https://api.mainnet-beta.solana.com')UI customisation
Kevo's modal and dashboard are fully themeable from the portal (Project → Settings → Appearance), or from code by passing a uiConfig override. The following tokens are supported:
| Token | Description | Default |
|---|---|---|
| accentColor | Buttons, links, focus rings | #111827 |
| backgroundColor | Modal background | #ffffff |
| foregroundColor | Text | #111827 |
| borderRadius | Corner radius in px | 12 |
| logoUrl | Logo shown at top of modal | — |
| fontFamily | CSS font-family string | system-ui |
| buttonTextColor | Text on primary buttons | — |
| inputBorderColor | Input field border | — |
| modalTitle | Override the modal heading | "Welcome" |
| subtitleText | Override the subtitle | — |
| showPoweredBy | Show "Powered by Kevo" | true |
| overlayBlur | Backdrop blur in px | 2 |
| modalMaxWidth | Max modal width in px | 400 |
| socialButtonStyle | 'outline' or 'filled' | 'outline' |
| hideSigningConfirmations | Skip signing confirmation modals | false |
<KevoDashboard>
A full account management page (wallet address, auth methods, key export, etc.) that can be embedded anywhere:
import { KevoDashboard, useKevoDashboard } from '@kevo-ws/sdk/react'
// Controlled
const { open, close, isOpen } = useKevoDashboard()
// Uncontrolled embed
<KevoDashboard />Vanilla JS / framework-agnostic
Use KevoClient directly without React:
import { KevoClient } from '@kevo-ws/sdk'
const client = new KevoClient({
publishableKey: 'pk_live_...',
evmRpcUrl: 'https://mainnet.infura.io/v3/YOUR_KEY',
})
// Mount the hidden signing iframe (required before any signing operation)
client.mount()
// Auth
await client.sendEmailOtp('[email protected]')
await client.verifyEmailOtp('[email protected]', '123456')
// Wallet
console.log(client.wallet?.address) // EVM
console.log(client.solanaWallet?.address) // Solana
// Sign
const sig = await client.signMessage('hello')
const txHash = await client.sendTransaction({ to: '0x…', chainId: 1 })
// Subscribe to session changes
const unsubscribe = client.onChange(() => {
console.log('session changed', client.session)
})
// Cleanup
client.unmount()
unsubscribe()Configuration reference
KevoConfig (client)
| Option | Type | Default | Description |
|---|---|---|---|
| publishableKey | string | required | Your project's publishable key (pk_live_… or pk_test_…) |
| apiUrl | string | https://api.kevo.ws | Kevo API base URL |
| iframeUrl | string | https://iframe.kevo.ws | Signing iframe URL |
| iframeTimeoutMs | number | 30000 | Timeout for iframe operations |
| chains | Record<number, string> | — | Multi-chain EVM RPC map — { 1: 'https://...', 8453: 'https://...' } |
| defaultChainId | number | first key in chains | Active chain on startup |
| evmRpcUrl | string | — | Single-chain EVM RPC (ignored when chains is set) |
| solanaRpcUrl | string | — | Default Solana JSON-RPC endpoint |
KevoAdminConfig (server)
| Option | Type | Default | Description |
|---|---|---|---|
| secretKey | string | required | Your project's secret key (sk_live_…) |
| apiUrl | string | https://api.kevo.ws | Kevo API base URL |
| evmRpcUrl | string | — | Default EVM JSON-RPC endpoint |
| solanaRpcUrl | string | — | Default Solana JSON-RPC endpoint |
TypeScript reference
All types are exported from @kevo-ws/sdk:
import type {
KevoConfig,
EvmChains, // Record<number, string> — multi-chain RPC map
KevoSession,
KevoWallet,
KevoSolanaWallet,
KevoAnyWallet,
TransactionRequest,
Eip712TypedData,
AuthMethod, // 'email' | 'google' | 'x' | 'apple'
SupportedChain, // 'evm' | 'solana'
KevoProjectConfig,
KevoDelegation,
DelegationPolicies,
GrantDelegationOptions,
SolanaProvider,
} from '@kevo-ws/sdk'Server types from @kevo-ws/sdk/server:
import type {
KevoAdminConfig,
AdminUser,
AdminUserDetail,
AdminDelegation,
SignEvmOptions,
SignEvmResult,
SignSolanaOptions,
SignSolanaResult,
SendTransactionOptions,
SendSolanaTransactionOptions,
WebhookPayload,
DelegationPolicies,
} from '@kevo-ws/sdk/server'Support
- dashboard: portal.kevo.ws
- Documentation: docs.kevo.ws
