Skip to content

KashDAO/protocol-sdk-typescript

Repository files navigation

@kashdao/protocol-sdk

Self-orchestrated, non-custodial TypeScript SDK for the Kash prediction-market protocol.

npm version types license Node 22+ viem

🧪 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.

Two trading modes — pick one

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);
}

Supported chains

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: 8453 is 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.

Local Anvil / Hardhat / Tenderly forks / sidechains

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/.

When to use this package

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. See HUMMINGBOT_INTEGRATION.md for a full strategy walkthrough.

Surface

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 signer

SA-mode params.account validation. Every trades.{build,prepare,send}.* call validates that params.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 matching options.salt to point the validator at the right derivation; otherwise it throws KashConfigError(SA_ACCOUNT_MISMATCH). EOA mode does the same check synchronously (params.account === signer.ownerAddress) via the *_ACCOUNT_MISMATCH error 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.outcomes is 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's markets.get(id).outcomeLabels. Same applies to quote.outcomeIndex and BuildBuyParams.outcome.

Reference signer adapters:

EOA mode — implements signTransaction(tx) → signedSerializedTx:

  • viemAccountEoaSigner(account) — wraps a viem Account (privateKeyToAccount, mnemonicToAccount, etc.).
  • jsonRpcEoaSigner({ rpc, address }) — calls remote eth_signTransaction over 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 viem Account.
  • jsonRpcSigner({ rpc, address }) — calls remote personal_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 signing
  • fireblocks-signer.ts — Fireblocks for smart-account mode
  • fireblocks-eoa-signer.ts — Fireblocks for EOA mode
  • privy-signer.ts — Privy: server-side adapter + browser-side viemAccountSigner pattern (Privy's embedded wallet exposes a viem Account, 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 patterns
  • web3signer-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 id 0x1); 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';

Install

npm install @kashdao/protocol-sdk viem
# or
pnpm add @kashdao/protocol-sdk viem

viem is a peer dependency. zod ships transitively. No internal Kash packages are pulled in.

Quickstart — smart-account mode

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 (see addresses.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.

Quickstart — EOA mode

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.transferFrom reverts 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.ts and examples/smart-account/09-first-trade-approval.ts for full worked flows.

Power users — explicit trade lifecycle

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.

Real-time market events

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();

Error handling

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 retry
  • isOperationaltrue for expected operational failures (network blip, slippage revert), false for programmer errors (misconfigured client)
  • cause — the underlying error (network failure, viem revert, etc.)

What this SDK deliberately does NOT do

  • 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.

Where each method goes — zero Kash dependency

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.

Need market discovery or trade history?

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.

Architectural choices

  • ABIs are vendored. No internal @kashdao/* runtime deps — every ABI lives under src/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.ts is a static literal of the Kash protocol's per-chain registry. A drift test asserts equality against the canonical source.
  • SmartAccountSignerAdapter is sign-the-hash by default, with an optional signTypedDataV4 path for HSM/Fireblocks/AWS-KMS integrations that don't expose raw hash signing. EOA mode uses EoaSignerAdapter (signs full EIP-1559 transactions).

Long-form integration guide

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.

License

MIT — see LICENSE.

About

Non-custodial TypeScript SDK for the Kash prediction-market protocol. Bring your own RPC, signer, and bundler. Native fetch + viem; zero internal dependencies.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors