Self-orchestrated, non-custodial TypeScript SDK for the Kash prediction-market protocol.
🧪 Staging release. Base mainnet (8453) protocol contracts are not yet deployed; only Base Sepolia (84532) is supported today. See Supported chains for the mainnet timeline.
Trust model — non-custodial AND zero Kash dependency. Every Kash SDK and the public API are non-custodial: user funds always live in accounts the user controls, and Kash never holds funds, never moves funds, never holds keys, and never signs anything. What makes this package distinct is that it also has zero Kash dependency: it never reaches Kash-hosted infrastructure. Everything required to use it lives on the consumer's side — their RPC, their signer, (optionally) their bundler, their funded address. See SECURITY.md § Non-custodial design for the formal statement.
The SDK supports both wallet models the protocol allows. Pick the one that matches your stack:
| Mode | Factory | Best for |
|---|---|---|
| EOA (vanilla EIP-1559) | createEoaClient |
Market makers with existing tx-signing infra (web3signer, Fireblocks-direct, AWS-KMS, ethers wallet). No bundler. Lowest per-trade overhead. |
| Smart Account (ERC-4337 v0.7) | createSmartAccountClient |
Self-custody UIs, AI-agent runners, retail-adjacent integrators that benefit from gasless onboarding via paymaster (PaymasterConfig injection), social recovery, or session keys. (Batched UserOps require a custom SimpleAccount variant with executeBatch — not in scope for this SDK.) |
Both modes are first-class peers, share the on-chain read surface, and preserve the non-custodial guarantee (user funds in Privy-managed MPC smart accounts; the consumer controls signing). You can use one or both side-by-side.
// EOA mode
import { createEoaClient, viemAccountEoaSigner } from '@kashdao/protocol-sdk';
const eoa = createEoaClient({ chainId: 84532, rpc, signer: viemAccountEoaSigner(account) });
// Smart-account mode
import { createSmartAccountClient, viemAccountSigner } from '@kashdao/protocol-sdk';
const sa = createSmartAccountClient({
chainId: 84532,
rpc,
signer: viemAccountSigner(account),
bundler: '...',
});If you want generic code that handles either, use the TradingClient discriminated union:
import { type TradingClient, isEoaClient } from '@kashdao/protocol-sdk';
function reportClient(client: TradingClient) {
if (isEoaClient(client)) console.log('EOA at', client.signer.ownerAddress);
else console.log('SA bundler', client.bundler.entryPointAddress);
}| Chain | chainId |
Status today | Notes |
|---|---|---|---|
| Base Sepolia (testnet) | 84532 |
✅ Usable — full surface | Recommended for integration work |
| Base (mainnet) | 8453 |
🟡 Coming soon | Protocol contracts not yet deployed. createSmartAccountClient / createEoaClient throw KashConfigError synchronously. Track at protocol-sdk-typescript/issues. |
Programmatic gating:
import {
isSupportedChainId, // strict: createXxxClient will succeed
isKnownChainId, // loose: SDK has metadata, may not be deployed yet
SUPPORTED_CHAIN_IDS, // [84532]
KNOWN_CHAIN_IDS, // [84532, 8453]
} from '@kashdao/protocol-sdk';
if (isSupportedChainId(chainId)) renderTradeUI();
else if (isKnownChainId(chainId)) renderComingSoonBanner();
else renderUnsupportedChainError();Safety guarantee. Until mainnet deploys, any attempt to use
chainId: 8453is a synchronous, typed throw at the factory (both modes), and at the Zod parse layer before that. No funds at risk; no transaction or UserOp can be built, signed, or submitted against an undeployed protocol.
For chains outside the static registry, pass customChain on the client config — it bypasses the registry (and the mainnet-not-deployed guard) and uses the addresses you supply verbatim. Mirrors viem's defineChain pattern.
import { defineChain } from 'viem';
import { createSmartAccountClient, viemAccountSigner } from '@kashdao/protocol-sdk';
const client = createSmartAccountClient({
chainId: 31337,
rpc: 'http://localhost:8545',
signer: viemAccountSigner(account),
customChain: {
name: 'Anvil',
viemChain: defineChain({
id: 31337,
name: 'Anvil',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: ['http://localhost:8545'] } },
}),
addresses: { factory: '0x…', usdc: '0x…' /* …rest from your deploy file */ },
// SA mode only — Anvil's SA factory differs from canonical (see TROUBLESHOOTING.md § AA23)
smartAccount: {
factoryAddress: '0x…',
implementationAddress: '0x…',
entryPointAddress: '0x…',
},
},
});Read TROUBLESHOOTING.md before running
locally — covers the AA23 / EntryPoint deployment / USDC decimals
traps that bite every new local-dev integration. Working examples
for both modes live in examples/local-anvil/.
| You are… | Mode | Install |
|---|---|---|
| A market maker with existing tx-signing infra (HSM, Fireblocks, web3signer, AWS-KMS) | EOA | @kashdao/protocol-sdk |
| An AI-agent runner with a viem account or remote signer | EOA or SA | @kashdao/protocol-sdk |
| A self-custody UI builder wanting paymaster / batched ops / session keys | SA | @kashdao/protocol-sdk |
| A Hummingbot strategy author (or any Python market-making framework) | EOA | kashdao-protocol-sdk (Python — the canonical path; mirrors this SDK byte-for-byte) |
| A developer building a retail integration | (use the Kash-orchestrated path instead) | @kashdao/sdk (wraps the public REST API) |
Hummingbot users: install the Python SDK. It exposes the same factories (
create_eoa_client,create_smart_account_client), the same trade lifecycle, and the same error hierarchy as this package — same encoding, same chain semantics. SeeHUMMINGBOT_INTEGRATION.mdfor a full strategy walkthrough.
Both client factories return mode-specific types but share the same conceptual surface. The trade lifecycle is exposed at three layers — pick the highest one that fits your control needs:
// Common to both modes
client.mode // 'eoa' | 'smart-account' (discriminator)
client.markets.{get, state, quote, watch} // 100% on-chain via your RPC
client.account.{usdcBalance, usdcAllowance, position, gasBalance}
// trades — ordered shortest-path → most explicit
client.trades.send.{buy, sell, closePosition, approve} // 1-call: prepare + simulate + sign + submit + wait
client.trades.{prepareBuy, prepareSell, prepareClosePosition, prepareApprove} // build + estimate gas + estimate fees + recompute hash
client.trades.{buildBuy, buildSell, buildClosePosition, buildApprove} // unsigned tx/UserOp only
client.trades.{simulate, hashOf, submit} // inspect + raw forward
client.trades.waitForReceipt(hash, { signal?, timeoutMs? }) // mode-agnostic — works on EOA tx hashes AND UserOp hashes
client.chainId // configured chain id (number)
client.signer // your signer adapter (mode-specific contract)
client.publicClient // raw viem PublicClient if you need to escape
client.addresses // per-chain protocol address registry
client.rpc.health() // chain-RPC liveness probe (returns { ok, chainId, latencyMs })
client.rpc.estimateFees(opts?) // EIP-1559 fee estimator — symmetric across both modes
// defaultFees / aggressiveFees / replacementFees presets exported separately
// Smart-account mode only
client.bundler // ERC-4337 v0.7 RPC client (your bundler)
client.bundler.health() // bundler liveness probe — symmetric with client.rpc.health()
client.account.computeAddress // derive deterministic SA address
client.account.isDeployed // check on-chain deployment status
client.account.ensureDeployed // returns { deployed, address } — surface "first trade includes setup" hint to UIs
// EOA mode only — no bundler / no SA derivation; the EOA address IS the signerSA-mode
params.accountvalidation. Everytrades.{build,prepare,send}.*call validates thatparams.account === computeAddress(signer.ownerAddress, options.salt ?? 0n). Catches the most common SA-mode footgun (pasting the EOA owner instead of the derived SA). If you run multiple SAs per EOA via different salts, pass the matchingoptions.saltto point the validator at the right derivation; otherwise it throwsKashConfigError(SA_ACCOUNT_MISMATCH). EOA mode does the same check synchronously (params.account === signer.ownerAddress) via the*_ACCOUNT_MISMATCHerror codes.
Cross-market discovery (markets.list) and historical trade aggregation (markets.predictions) are not in this package — neither is feasible on-chain at scale, and including them would introduce a Kash-API dependency that breaks the zero-Kash-dependency guarantee this package makes (the non-custodial guarantee is preserved either way). If you need them, install @kashdao/sdk alongside; it wraps the public REST API.
Outcome labels (
"YES","NO", candidate names) are off-chain — they live in market metadata and are not on the Market contract.state.outcomesis positional: index 0 is "the first outcome" and consumers either know the position-to-label mapping out-of-band, hard-code it for boolean markets, or fetch it from@kashdao/sdk'smarkets.get(id).outcomeLabels. Same applies toquote.outcomeIndexandBuildBuyParams.outcome.
Reference signer adapters:
EOA mode — implements signTransaction(tx) → signedSerializedTx:
viemAccountEoaSigner(account)— wraps a viemAccount(privateKeyToAccount,mnemonicToAccount, etc.).jsonRpcEoaSigner({ rpc, address })— calls remoteeth_signTransactionover consumer-provided RPC. Suitable for web3signer, Frame, MetaMask-style providers, Fireblocks-direct, AWS-KMS via wrappers.
Smart-account mode — implements signUserOpHash(hash) → sig (and optional signTypedDataV4):
viemAccountSigner(account)— wraps a viemAccount.jsonRpcSigner({ rpc, address })— calls remotepersonal_sign/eth_signTypedData_v4.
For HSM / Fireblocks / AWS-KMS / proprietary signers, implement the appropriate EoaSignerAdapter or SmartAccountSignerAdapter interface yourself. Worked examples (copy into your repo and adapt the imports) live at examples/signers/:
aws-kms-signer.ts— AWS KMS raw signingfireblocks-signer.ts— Fireblocks for smart-account modefireblocks-eoa-signer.ts— Fireblocks for EOA modeprivy-signer.ts— Privy: server-side adapter + browser-sideviemAccountSignerpattern (Privy's embedded wallet exposes a viemAccount, so the built-in adapter is enough — no custom code needed for the React/browser path)coinbase-smart-wallet-signer.ts— Coinbase Smart Wallet integration patternsweb3signer-signer.ts— Consensys web3signer over JSON-RPC
Bundler clients (smart-account mode only — bundler is required for any trades.* or bundler.* call but optional at construction time, so read-only markets.* / account.* consumers can build the client without one. The first trade attempt without a bundler throws KashConfigError(BUNDLER_REQUIRED). No default URL — bundlers are chain-scoped and a one-size-fits-all default would silently reject UserOps for the chain id you configured):
createFlashbotsBundlerClient({ url })— Ethereum mainnet only as of writing (the canonical Flashbots Protect URL returns chain id0x1); pass an explicit URL.createPimlicoBundlerClient({ url, apiKey })— supports Base + Base Sepolia + others.createAlchemyBundlerClient({ url, apiKey })— supports Base + Base Sepolia + others.createGenericBundlerClient({ url })— any compliant ERC-4337 v0.7 bundler RPC.
Subpath imports for tree-shaken bundles:
// Smart-account mode
import { createPimlicoBundlerClient } from '@kashdao/protocol-sdk/bundler/pimlico';
import { jsonRpcSigner } from '@kashdao/protocol-sdk/signers/json-rpc';
// EOA mode
import { viemAccountEoaSigner } from '@kashdao/protocol-sdk/eoa/signers/viem-account';
import { jsonRpcEoaSigner } from '@kashdao/protocol-sdk/eoa/signers/json-rpc';npm install @kashdao/protocol-sdk viem
# or
pnpm add @kashdao/protocol-sdk viemviem is a peer dependency. zod ships transitively. No internal Kash packages are pulled in.
The shortest path: client.trades.send.* packs build → simulate →
sign → submit → wait into one call. For 95% of consumers this is the
right entry point. Power users wanting per-step control drop down to
the explicit lifecycle (see "Power users — explicit trade lifecycle"
below).
Need testnet funds? Base Sepolia (chain id 84532) faucets: ETH for gas — Coinbase Base Sepolia faucet or Alchemy Sepolia faucet. USDC for trades — Kash on Base Sepolia uses a mock USDC contract at
0xD6F9b17fB20aB2E532ACBAB4C7eeCc0915278913(seeaddresses.ts), not Circle's official testnet USDC. Mint or transfer the mock token to your trading address; Circle's official testnet USDC is unrelated and will not work with the protocol contracts.
import { createSmartAccountClient, usdc, viemAccountSigner } from '@kashdao/protocol-sdk';
import { privateKeyToAccount } from 'viem/accounts';
const owner = privateKeyToAccount(process.env.KASH_OWNER_KEY as `0x${string}`);
const client = createSmartAccountClient({
chainId: 84532, // Base Sepolia
rpc: 'https://my-base-sepolia-node.example.com',
signer: viemAccountSigner(owner),
// Bundler is required and chain-scoped — bring your own. For Base
// Sepolia, Pimlico, Alchemy, and Stackup are the common picks.
bundler: { provider: 'pimlico', url: process.env.KASH_BUNDLER_URL! },
});
// Derive the smart-account address (deterministic CREATE2).
const sa = await client.account.computeAddress(owner.address);
// Read on-chain market state.
const state = await client.markets.state(knownMarketAddress);
console.log(state.outcomes.map((o) => o.probability));
// One-line trade. Behind the scenes: build → estimate gas → estimate
// fees → recompute hash → simulate → sign → submit → wait for inclusion.
// Auto-deploys the SA on first use.
const result = await client.trades.send.buy(knownMarketAddress, {
account: sa,
outcome: 0,
amountUsdc: usdc('10'), // 10 USDC, no atomic-math gymnastics
maxSlippageBps: 50, // 0.5%
});
if ('blockNumber' in result) {
console.log(`included: ${result.transactionHash} block ${result.blockNumber}`);
}First-time consumers: the trade above will revert if the SA hasn't
approved USDC to the Market yet. See the "First-trade approval" note
below or examples/smart-account/09-first-trade-approval.ts for the
canonical usdcAllowance → conditional approve → buy flow.
Same one-line pattern as smart-account mode — same params shape, same return type. The signer signs vanilla EIP-1559 transactions; no bundler involved.
import { createEoaClient, usdc, viemAccountEoaSigner } from '@kashdao/protocol-sdk';
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.KASH_OWNER_KEY as `0x${string}`);
const client = createEoaClient({
chainId: 84532,
rpc: 'https://my-base-sepolia-node.example.com',
signer: viemAccountEoaSigner(account),
});
const result = await client.trades.send.buy(knownMarketAddress, {
account: client.signer.ownerAddress, // EOA holds USDC + receives tokens
outcome: 0,
amountUsdc: usdc('1'), // 1 USDC
maxSlippageBps: 50, // 0.5%
});
if ('blockNumber' in result) {
console.log(`included: ${result.transactionHash} block ${result.blockNumber}`);
}First-trade approval (already shipped). Both modes need a one-time
USDC.approve(market, max)before the first BUY —Market.transferFromreverts otherwise. The SDK ships the canonical pattern:import { MAX_UINT256 } from '@kashdao/protocol-sdk'; const allowance = await client.account.usdcAllowance(account, marketAddress); if (allowance < amountUsdc) { await client.trades.send.approve({ account, spender: marketAddress, amount: MAX_UINT256, // approve once, trade forever }); } await client.trades.send.buy(marketAddress, { account, outcome: 0, amountUsdc, maxSlippageBps: 50, });See
examples/eoa/03-approve-and-buy.tsandexamples/smart-account/09-first-trade-approval.tsfor full worked flows.
send.* covers 95% of consumers. For the other 5% — custom fee
strategies, replacement-by-fee, batched sponsor sign-offs,
mode-specific orchestration — drop down to the explicit lifecycle:
| Step | Method | Purpose |
|---|---|---|
| 1 | client.trades.buildBuy/Sell/ClosePosition/Approve |
Encode calldata, read nonce, return zeroed-gas UserOp/tx |
| 2 | client.bundler.estimateGas (SA) / client.publicClient.estimateGas (EOA) |
Populate gas limits |
| 3 | client.bundler.estimateFees (SA) / client.estimateFees (EOA) |
Populate maxFeePerGas / maxPriorityFeePerGas |
| 4 | client.trades.simulate |
Pre-flight eth_call; surfaces typed revert reasons |
| 5 | client.trades.hashOf |
Recompute the canonical hash AFTER population (the build-time hash is stale) |
| 6 | client.signer.signUserOpHash (SA) / client.signer.signTransaction (EOA) |
Sign |
| 7 | client.trades.submit |
Submit; pre-flight staleness guard catches stale-hash mistakes |
| 8 | client.trades.waitForReceipt (mode-agnostic) — or client.bundler.waitForReceipt (SA) / client.publicClient.waitForTransactionReceipt (EOA) for raw mode-specific receipt fields |
Wait for inclusion |
Or use client.trades.prepareBuy/Sell/ClosePosition/Approve to
collapse steps 1–5 into one call, returning a fully-populated
BuiltUserOp / BuiltTransaction ready to sign.
See examples/smart-account/05-build-sign-submit.ts and
examples/eoa/05-build-sign-submit.ts for the full explicit flow.
const sub = client.markets.watch(marketAddress, {
onEvent(ev) {
if (ev.type === 'TRADE') console.log(`${ev.side} #${ev.outcome}: ${ev.tokensWad} tokens`);
if (ev.type === 'RESOLVED') console.log(`winner: outcome ${ev.winningOutcome}`);
},
onReconnect() {
// Best-effort delivery — viem reconnects but does NOT replay missed events.
// Reconcile via @kashdao/sdk's kash.markets.predictions if gap-free
// coverage matters, or your own indexer.
},
onError(err) {
console.error(err.code, err.message);
},
onConnectionChange(state) {
// Edge-triggered on transitions only ('connected' | 'reconnecting' | 'unsubscribed').
metrics.gauge('kash.watch.connection', state === 'connected' ? 1 : 0);
},
});
// Introspection — call at any time, no polling required:
sub.isHealthy(); // true if currently connected
sub.connectionState(); // 'connected' | 'reconnecting' | 'unsubscribed'
sub.lastBlockSeen(); // bigint | null — high-water mark for reconciliation queries (null until first event)
// Later: sub.unsubscribe();Every error extends KashProtocolError. Use the cross-realm-safe static is:
import { KashProtocolError } from '@kashdao/protocol-sdk';
try {
await client.trades.submit(signedUserOp);
} catch (err) {
if (KashProtocolError.is(err)) {
console.error(err.code, err.context, err.cause);
if (err.isRetryable) await sleepThenRetry();
} else {
throw err;
}
}Each error carries:
code— stable machine-readable identifier (e.g.,BUNDLER_RPC_ERROR,MARKET_READ_FAILED)context— free-form diagnostic data (URL, market address, etc.)isRetryable— whether the consumer should retryisOperational—truefor expected operational failures (network blip, slippage revert),falsefor programmer errors (misconfigured client)cause— the underlying error (network failure, viem revert, etc.)
- No retries on UserOp submission. Retrying a transaction can cause double-spends. Implement retries at your layer with your own idempotency.
- No telemetry to Kash. No phone-home; no usage analytics.
- No paymaster. The smart account pays its own gas in ETH.
- No relay. Signed UserOps go to the consumer-configured bundler, never to a Kash-hosted service.
Every operation in this package goes through either the consumer's RPC or the consumer's bundler. Kash-hosted infrastructure is never in the path.
| Method | Goes to |
|---|---|
markets.{get,state,quote,watch} |
Consumer's RPC (on-chain readContract / watchContractEvent) |
account.{usdcBalance,usdcAllowance,position,gasBalance} (+ SA-only computeAddress,isDeployed,ensureDeployed) |
Consumer's RPC |
trades.{buildBuy,buildSell,buildClosePosition,buildApprove,simulate} |
Consumer's RPC (quote + nonce + simulation reads) |
trades.{prepareBuy,prepareSell,prepareClosePosition,prepareApprove} |
Consumer's RPC (build + gas + fee estimation) |
trades.submit + bundler.* (SA only) + trades.send.* |
Consumer's bundler RPC (SA) / consumer's chain RPC (EOA) |
trades.waitForReceipt(hash) |
Consumer's bundler RPC (SA) / consumer's chain RPC (EOA) |
rpc.{health,estimateFees} |
Consumer's RPC |
Kash literally cannot see, censor, or front-run anything done through this SDK. If the Kash API goes down, this SDK is unaffected.
Those operations aren't in this package by design — neither is feasible on-chain at scale (enumerating all markets requires scanning factory creation events back to startBlock; per-market history requires unbounded log queries). They live in @kashdao/sdk, which wraps the public REST API:
import { KashClient } from '@kashdao/sdk';
import { createSmartAccountClient } from '@kashdao/protocol-sdk';
const kash = new KashClient({ apiKey: process.env.KASH_API_KEY });
const direct = createSmartAccountClient({ chainId: 84532, rpc, signer, bundler });
// Discovery + history via the Kash-orchestrated sdk (REST)
const markets = await kash.markets.list({ status: 'ACTIVE' });
const recent = await kash.markets.predictions(markets.data[0].id);
// Trade execution via protocol-sdk (zero Kash dependency)
const built = await direct.trades.buildBuy(markets.data[0].contractAddress, params);You can run the two packages side-by-side, or skip @kashdao/sdk entirely if you're a market maker maintaining your own market registry and indexer.
- ABIs are vendored. No internal
@kashdao/*runtime deps — every ABI lives undersrc/contracts/generated/and is regenerated from the Kash protocol's canonical ABI source. A drift test in CI fails the build if the vendored tree disagrees with the source. - Addresses are mirrored.
src/contracts/addresses.tsis a static literal of the Kash protocol's per-chain registry. A drift test asserts equality against the canonical source. SmartAccountSignerAdapteris sign-the-hash by default, with an optionalsignTypedDataV4path for HSM/Fireblocks/AWS-KMS integrations that don't expose raw hash signing. EOA mode usesEoaSignerAdapter(signs full EIP-1559 transactions).
A more detailed market-maker walkthrough — covering wallet setup, gas funding, signer integration, and reconnection semantics — is published at docs.kash.bot/developer-docs/protocol-sdk/overview.
MIT — see LICENSE.