npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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


Installation

npm install @kevo-ws/sdk

React 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 automatically
  • useTokenBalance('0x...') fetches from Base automatically
  • sendTransaction({ to, value, chainId: 8453 }) routes to Base — no rpcUrl argument 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  })  // Polygon

Real-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 polling

All 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