name: starkzap-sdk description: "Use when integrating or maintaining applications built with keep-starknet-strange/starkzap. Covers StarkSDK setup, onboarding (Signer/Privy/Cartridge), wallet lifecycle, sponsored transactions, ERC20 transfers, staking flows, tx builder batching, examples, tests, and generated presets." license: Apache-2.0 metadata: author: keep-starknet-strange version: "1.0.0" org: keep-starknet-strange compatibility: "Node.js 20+, TypeScript 5+, starkzap repository workflows" keywords:
- starknet
- starkzap
- sdk
- typescript
- onboarding
- wallet
- privy
- cartridge
- paymaster
- erc20
- staking
- tx-builder allowed-tools:
- Bash
- Read
- Write
- Glob
- Grep
- Task user-invocable: true
Starkzap SDK
Project-focused guide for https://github.com/keep-starknet-strange/starkzap.
Use this skill when requests involve Starkzap SDK code, examples, or docs.
When To Use
Trigger for tasks like:
- "Add a new onboarding flow in Starkzap"
- "Fix sponsored transaction behavior in wallet.execute"
- "Update staking pool logic or validator presets"
- "Patch Privy signer/server integration"
- "Review Starkzap tests/docs/examples"
SDK Scope — What Is and Is NOT Included
The starkzap SDK covers wallet connectivity, account management, ERC20 operations, staking delegation, and transaction building. It does NOT include:
- Swap / DEX aggregation (use AVNU or a separate swap SDK)
- Lending / Borrowing (use zkLend, Nostra, or direct contract calls)
- DCA (Dollar Cost Averaging)
- NFT operations
- Bridge operations
- Yield farming / Liquidity provision
For general DeFi operations (swaps, DCA, lending), use a separate integration. The starkzap wallet's wallet.execute() and wallet.callContract() can interact with any Starknet contract directly.
Repository Map
Primary implementation:
src/sdk.ts- top-level orchestration (StarkSDK)src/wallet/*- wallet implementations and lifecyclesrc/signer/*-StarkSigner,PrivySigner, signer adaptersrc/tx/*-TxandTxBuildersrc/erc20/*- token helpers, balance/transfer logicsrc/staking/*- staking operations and pool discoverysrc/types/*- shared domain types (Address,Amount, config)
Operational and docs:
tests/*andtests/integration/*examples/web,examples/server,examples/mobile,examples/flappy-birdscripts/*for generated artifacts in the Starkzap repodocs/*andmintlify-docs/*
Skill resources:
skills/starkzap-sdk/references/signer-integration.md- signer trust boundaries and auth assumptionsskills/starkzap-sdk/references/sponsored-transactions.md- paymaster flow and fee mode behaviorskills/starkzap-sdk/references/erc20-helpers.md-Amountsemantics and transfer patternsskills/starkzap-sdk/references/staking-reliability.md- pool discovery and timeout/abort safetyskills/starkzap-sdk/scripts/wallet-execute-example.ts- wallet readiness and execute flowskills/starkzap-sdk/scripts/staking-pool-discovery.ts- staking pool discovery and diagnostics
Common Pitfalls — READ BEFORE WRITING CODE
These are exact API gotchas discovered during real integration. Violating any of these causes type errors or runtime failures.
1. Address is a branded type — always use fromAddress()
Address is string & { readonly __type: "StarknetAddress" }. You CANNOT pass a plain string where Address is expected.
import { fromAddress } from "starkzap";
// WRONG — will not compile
await wallet.stakingInStaker("0x123...", token);
// CORRECT
await wallet.stakingInStaker(fromAddress("0x123..."), token);
2. Validator presets are objects, not arrays — no getValidators() function
There is NO getValidators() function. Validators are exported as keyed objects.
import { sepoliaValidators, mainnetValidators } from "starkzap";
// WRONG — does not exist
const validators = getValidators("sepolia");
// CORRECT — use Object.values() to get an array
const validators = Object.values(sepoliaValidators);
// Each entry: { name: string, stakerAddress: Address, logoUrl: URL | null }
3. Token presets are objects, not arrays — no getTokens() function
Same pattern as validators. Also available: getPresets(chainId) returns Record<string, Token>.
import { sepoliaTokens, mainnetTokens } from "starkzap";
// Get all tokens as array
const tokens = Object.values(sepoliaTokens);
// Each entry: { name, address: Address, decimals, symbol, metadata?: { logoUrl } }
// Or use the chainId-based helper
import { getPresets } from "starkzap";
const tokenMap = getPresets(ChainId.SEPOLIA); // Record<string, Token>
4. No wallet.activeTokens() — use sdk.stakingTokens() or import presets
// WRONG — method does not exist
const tokens = await wallet.activeTokens();
// CORRECT — for staking tokens only
const stakingTokens = await sdk.stakingTokens();
// CORRECT — for all known tokens
import { sepoliaTokens } from "starkzap";
const allTokens = Object.values(sepoliaTokens);
5. Staking class methods require wallet as first parameter
Every mutation and query method on the Staking class takes wallet as its first argument.
// WRONG — missing wallet parameter
await staking.stake(amount);
await staking.getPosition();
await staking.claimRewards();
// CORRECT
await staking.stake(wallet, amount);
await staking.getPosition(wallet);
await staking.claimRewards(wallet);
await staking.exitIntent(wallet, amount);
await staking.exit(wallet);
await staking.isMember(wallet);
// EXCEPTION — getCommission() does NOT take wallet
await staking.getCommission(); // returns Promise<number>
6. wallet.stakingInStaker() requires BOTH stakerAddress AND token
// WRONG — missing token parameter
const staking = await wallet.stakingInStaker(stakerAddress);
// CORRECT
const staking = await wallet.stakingInStaker(fromAddress(stakerAddress), token);
7. PoolMember fields are Amount objects, unpoolTime is Date | null
const pos = await staking.getPosition(wallet);
if (!pos) return; // null means no position
// WRONG — these are Amount objects, not numbers or bigints
if (pos.staked > 0) { ... }
// CORRECT — use Amount methods
if (pos.staked.isPositive()) { ... }
pos.staked.toUnit(); // "1.5" — human-readable string
pos.staked.toFormatted(); // "1.5 STRK" — formatted with symbol
pos.staked.toBase(); // 1500000000000000000n — raw bigint
pos.staked.isZero(); // boolean
// unpoolTime is Date | null, NOT a unix timestamp
if (pos.unpoolTime) {
const unixSeconds = Math.floor(pos.unpoolTime.getTime() / 1000);
}
// Full PoolMember shape:
// {
// staked: Amount,
// rewards: Amount,
// total: Amount,
// unpooling: Amount,
// unpoolTime: Date | null,
// commissionPercent: number,
// rewardAddress: Address,
// }
8. wallet.balanceOf() returns Amount, not a string or bigint
const balance = await wallet.balanceOf(token);
// WRONG
console.log(`Balance: ${balance} STRK`);
// CORRECT
console.log(`Balance: ${balance.toUnit()} STRK`); // "1.5 STRK"
console.log(`Balance: ${balance.toFormatted()}`); // "1.5 STRK" (includes symbol)
9. OnboardResult returns an object, not the wallet directly
// WRONG
const wallet = await sdk.onboard({ ... });
// CORRECT
const result = await sdk.onboard({ ... });
const wallet = result.wallet;
// result also has: strategy, deployed (boolean), metadata?
10. Validator type uses stakerAddress and logoUrl
// The Validator interface:
// {
// name: string,
// stakerAddress: Address, // NOT "address"
// logoUrl: URL | null, // NOT string — use .toString() for display
// }
Quick Reference
Common starknet.js patterns (provider/account/call/execute/listen):
import { Account, Contract, RpcProvider } from "starknet";
const provider = await RpcProvider.create({
nodeUrl: process.env.RPC_URL!,
});
const account = new Account({
provider,
address: process.env.ACCOUNT_ADDRESS!,
signer: process.env.PRIVATE_KEY!,
cairoVersion: "1",
});
const contract = new Contract({
abi,
address: process.env.CONTRACT_ADDRESS!,
providerOrAccount: account,
});
await contract.call("balance_of", [account.address]); // read
const tx = await account.execute([
{
contractAddress: process.env.CONTRACT_ADDRESS!,
entrypoint: "do_work",
calldata: [],
},
]);
await provider.waitForTransaction(tx.transaction_hash);
// With Starkzap Tx wrapper
const submitted = await wallet.execute(calls, { feeMode: "user_pays" });
// Tx has: hash (string), explorerUrl (string)
const stop = submitted.watch(
({ finality, execution }) => console.log(finality, execution),
{ pollIntervalMs: 5000, timeoutMs: 120000 }
);
// stop(); // call to unsubscribe early
await submitted.wait(); // or just wait for finality
const receipt = await submitted.receipt(); // full receipt (cached)
Common error classes and immediate recovery:
| Error Class | Typical Signal | Immediate Recovery |
|---|---|---|
VALIDATION_ERROR | Invalid token decimals, Amount.parse(...) failure | Re-check token decimals/symbol, parse from known token preset, avoid mixing token types. |
UNDEPLOYED_ACCOUNT | Account is not deployed on wallet.execute(...) | Run wallet.ensureReady({ deploy: "if_needed" }) before user_pays writes. |
RPC_OR_NETWORK | timeout, 429, provider mismatch | Retry with backoff, confirm rpcUrl and chainId, switch to stable RPC for production. |
TX_REVERTED | preflight.ok === false or reverted receipt | Run wallet.preflight({ calls }), inspect reason, reduce batch size, verify call ordering. |
AUTH_OR_PERMISSION | Privy 401/403, invalid signature response | Verify signer server auth, headers/body resolver, and trusted serverUrl. |
See also:
skills/starkzap-sdk/references/*for implementation-specific troubleshootingskills/starkzap-sdk/scripts/*for runnable diagnostic examples
Core Workflows
1) Configure StarkSDK and Connect Wallets
Common API path:
- Instantiate
StarkSDKwithnetworkorrpcUrl + chainId. - Use
sdk.onboard(...)orsdk.connectWallet(...)orsdk.connectCartridge(...). - Call
wallet.ensureReady({ deploy: "if_needed" })before user-pays writes.
Supported onboarding strategies:
OnboardStrategy.SignerOnboardStrategy.PrivyOnboardStrategy.Cartridge
For Cartridge:
- Treat as web-only runtime.
- Expect popup/session behavior and policy scoping requirements.
sdk.connectCartridge()returnsCartridgeWalletInterfacewith extra methods:getController(),username().
import {
ChainId,
OnboardStrategy,
StarkSDK,
StarkSigner,
} from "starkzap";
const sdk = new StarkSDK({ network: "sepolia" });
const customSdk = new StarkSDK({
rpcUrl: process.env.RPC_URL!,
chainId: ChainId.SEPOLIA,
});
// Signer onboarding
const signerResult = await sdk.onboard({
strategy: OnboardStrategy.Signer,
account: { signer: new StarkSigner(process.env.PRIVATE_KEY!) },
feeMode: "user_pays",
deploy: "if_needed",
});
const wallet = signerResult.wallet; // WalletInterface
// Privy onboarding
const privyResult = await sdk.onboard({
strategy: OnboardStrategy.Privy,
privy: {
resolve: async () => ({
walletId: process.env.PRIVY_WALLET_ID!,
publicKey: process.env.PRIVY_PUBLIC_KEY!,
serverUrl: process.env.PRIVY_SIGNER_URL!,
}),
},
feeMode: "sponsored",
});
// Cartridge onboarding (web-only)
const cartridgeResult = await sdk.onboard({
strategy: OnboardStrategy.Cartridge,
cartridge: {
preset: "controller",
policies: [{ target: "0xTOKEN", method: "approve" }],
},
feeMode: "user_pays",
});
// cartridgeResult.wallet is WalletInterface
// Direct Cartridge connect (returns CartridgeWalletInterface with username())
const cartridgeWallet = await sdk.connectCartridge({
policies: [{ target: "0xTOKEN", method: "approve" }],
});
const username = await cartridgeWallet.username(); // string | undefined
// Direct signer connect
const wallet2 = await sdk.connectWallet({
account: { signer: new StarkSigner(process.env.PRIVATE_KEY!) },
feeMode: "sponsored",
});
await wallet2.ensureReady({ deploy: "if_needed" });
2) Complete Wallet API Reference
// --- Properties ---
wallet.address // Address (branded string)
// --- Account Info ---
wallet.getAccount() // starknet.js Account instance
wallet.getProvider() // RpcProvider
wallet.getChainId() // ChainId — use .toLiteral() for display
wallet.getFeeMode() // "user_pays" | "sponsored"
wallet.getClassHash() // string
// --- Deployment ---
await wallet.isDeployed() // boolean
await wallet.ensureReady({ deploy: "if_needed" }) // deploy if needed
await wallet.deploy() // explicit deploy → Tx
// --- Transactions ---
await wallet.execute(calls, { feeMode: "user_pays" }) // → Tx
await wallet.preflight({ calls, feeMode }) // → { ok: true } | { ok: false, reason }
await wallet.estimateFee(calls) // → EstimateFeeResponseOverhead
await wallet.callContract({ contractAddress, entrypoint, calldata }) // read-only
await wallet.signMessage(typedData) // → Signature
wallet.tx() // → TxBuilder (fluent)
await wallet.disconnect() // cleanup
// --- ERC20 ---
wallet.erc20(token) // → Erc20 instance (cached per token)
await wallet.balanceOf(token) // → Amount
await wallet.transfer(token, [{ to: Address, amount: Amount }]) // → Tx
// --- Staking (convenience methods on wallet) ---
await wallet.stakingInStaker(stakerAddress, token) // → Staking instance
await wallet.staking(poolAddress) // → Staking instance
await wallet.stake(poolAddress, amount) // → Tx (auto enter/add)
await wallet.enterPool(poolAddress, amount) // → Tx
await wallet.addToPool(poolAddress, amount) // → Tx
await wallet.claimPoolRewards(poolAddress) // → Tx
await wallet.exitPoolIntent(poolAddress, amount) // → Tx
await wallet.exitPool(poolAddress) // → Tx
await wallet.isPoolMember(poolAddress) // → boolean
await wallet.getPoolPosition(poolAddress) // → PoolMember | null
await wallet.getPoolCommission(poolAddress) // → number
3) Staking via the Staking Class
The Staking class wraps a specific pool contract. Create via wallet.stakingInStaker() or Staking.fromStaker().
Critical: All mutation/query methods take wallet as the first argument (except getCommission()).
import { fromAddress, Amount } from "starkzap";
import type { WalletInterface, Token, Staking } from "starkzap";
// Discover pool from validator address
const staking = await wallet.stakingInStaker(
fromAddress("0xVALIDATOR_STAKER_ADDRESS"),
strkToken
);
// Query
const commission = await staking.getCommission(); // number (e.g. 10 = 10%)
const isMember = await staking.isMember(wallet); // boolean
const position = await staking.getPosition(wallet); // PoolMember | null
// Stake (auto enter or add based on membership)
const amount = Amount.parse("100", strkToken);
const tx = await staking.stake(wallet, amount);
await tx.wait();
// Claim rewards
const claimTx = await staking.claimRewards(wallet);
await claimTx.wait();
// Exit (2-step: intent → wait cooldown → exit)
const exitTx = await staking.exitIntent(wallet, amount);
await exitTx.wait();
// ... wait for cooldown (check position.unpoolTime) ...
const completeTx = await staking.exit(wallet);
await completeTx.wait();
4) ERC20 Operations
import { Amount, fromAddress, sepoliaTokens } from "starkzap";
const USDC = sepoliaTokens.USDC; // Token object from presets
const amount = Amount.parse("25", USDC);
// Balance
const balance = await wallet.balanceOf(USDC); // Amount object
console.log(balance.toUnit()); // "100.5"
console.log(balance.toFormatted()); // "100.5 USDC"
console.log(balance.isPositive()); // true
// Transfer
const tx = await wallet.transfer(USDC, [
{ to: fromAddress("0xRECIPIENT"), amount },
]);
await tx.wait();
// Via Erc20 class (for building calls without executing)
const erc20 = wallet.erc20(USDC);
const approveCalls = erc20.populateApprove(fromAddress("0xSPENDER"), amount);
const transferCalls = erc20.populateTransfer([
{ to: fromAddress("0xRECIPIENT"), amount },
]);
5) Amount Class Reference
import { Amount } from "starkzap";
// Creation
const a = Amount.parse("1.5", token); // from human-readable + Token
const b = Amount.parse("1.5", 18, "STRK"); // from human-readable + decimals + symbol
const c = Amount.fromRaw(1500000000000000000n, token); // from raw bigint
// Conversion
a.toUnit(); // "1.5" — human-readable string
a.toBase(); // 1500000000000000000n — raw bigint
a.toFormatted(); // "1.5 STRK" — with symbol
a.getDecimals(); // 18
a.getSymbol(); // "STRK" | undefined
// Arithmetic (returns new Amount)
a.add(b); a.subtract(b);
a.multiply(2n); a.divide(3n);
// Comparison
a.eq(b); a.gt(b); a.gte(b);
a.lt(b); a.lte(b);
a.isZero(); a.isPositive();
6) TxBuilder — Batched Operations
// Approve + stake in one atomic transaction
const tx = await wallet.tx()
.stake(poolAddress, Amount.parse("100", STRK))
.send();
await tx.wait();
// Multi-transfer + claim rewards atomically
const tx2 = await wallet.tx()
.transfer(USDC, [
{ to: alice, amount: Amount.parse("50", USDC) },
{ to: bob, amount: Amount.parse("25", USDC) },
])
.claimPoolRewards(poolAddress)
.send();
// Mix helpers with raw calls
const tx3 = await wallet.tx()
.approve(STRK, dexAddress, amount)
.add({ contractAddress: dexAddress, entrypoint: "swap", calldata: [...] })
.transfer(USDC, { to: alice, amount: usdcAmount })
.send();
// Inspection before sending
const builder = wallet.tx().stake(poolAddress, amount);
console.log(builder.length); // number of operations
const calls = await builder.calls(); // resolved Call[]
const fee = await builder.estimateFee();
const preflight = await builder.preflight(); // { ok: true } | { ok: false, reason }
const sentTx = await builder.send(); // can only call send() once
7) Tx Class — Transaction Tracking
const tx = await wallet.execute(calls, { feeMode: "user_pays" });
tx.hash; // transaction hash string
tx.explorerUrl; // block explorer URL
await tx.wait(); // wait for ACCEPTED_ON_L2
// Or watch with real-time updates
const unsubscribe = tx.watch(
({ finality, execution }) => {
console.log(finality); // e.g. "ACCEPTED_ON_L2"
console.log(execution); // e.g. "SUCCEEDED" or "REVERTED"
},
{
pollIntervalMs: 4000,
timeoutMs: 300000,
onError: (err) => console.error(err),
}
);
const receipt = await tx.receipt(); // full receipt (cached after first fetch)
8) Execute Transactions (wallet.execute, wallet.preflight, wallet.tx)
Use:
wallet.execute(calls, options)for direct execution.wallet.preflight({ calls, feeMode })for simulation checks.wallet.tx()(TxBuilder) for batched operations with deterministic ordering.
const calls = [
{
contractAddress: process.env.CONTRACT_ADDRESS!,
entrypoint: "do_work",
calldata: [],
},
];
const preflight = await wallet.preflight({
calls,
feeMode: "user_pays",
});
if (!preflight.ok) {
throw new Error(`Preflight failed: ${preflight.reason}`);
}
const userPaysTx = await wallet.execute(calls, { feeMode: "user_pays" });
await userPaysTx.wait();
const sponsoredTx = await wallet.execute(calls, { feeMode: "sponsored" });
await sponsoredTx.wait();
const batchedTx = await wallet
.tx()
.add(...calls)
.send({ feeMode: "sponsored" });
await batchedTx.wait();
function getSdkErrorClass(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("not deployed")) return "UNDEPLOYED_ACCOUNT";
if (message.includes("timed out") || message.includes("429")) {
return "RPC_OR_NETWORK";
}
if (message.includes("signature") || message.includes("Privy")) {
return "AUTH_OR_PERMISSION";
}
if (message.includes("reverted") || message.includes("preflight")) {
return "TX_REVERTED";
}
if (message.includes("Invalid") || message.includes("Amount")) {
return "VALIDATION_ERROR";
}
return "UNKNOWN";
}
try {
await wallet.execute(calls, { feeMode: "user_pays" });
} catch (error) {
const kind = getSdkErrorClass(error);
if (kind === "UNDEPLOYED_ACCOUNT") {
await wallet.ensureReady({ deploy: "if_needed" });
}
throw error;
}
When changing execution behavior:
- Audit deploy vs execute path for undeployed accounts.
- Verify runtime constraints (
OnboardStrategy.Cartridgeis web-only). - Cover both
user_paysandsponsoredbranches in tests.
9) Examples + Integration Surfaces
Check for drift between:
examples/web/main.tsexamples/server/server.tsREADMEand docs links
Specifically verify endpoint and auth consistency for Privy + paymaster proxy flows.
Guardrails
Do not hand-edit generated files:
src/erc20/token/presets.tssrc/erc20/token/presets.sepolia.tssrc/staking/validator/presets.tssrc/staking/validator/presets.sepolia.tsdocs/api/**docs/export/**
Regenerate with scripts:
npm run generate:tokens
npm run generate:tokens:sepolia
npm run generate:validators
npm run generate:validators:sepolia
npm run docs:api
npm run docs:export
Keep API export changes explicit:
- If new public API is added/removed, update
src/index.ts.
Validation Checklist
Run minimal set first:
npm run typecheck
npm test
Run broader checks when behavior is cross-cutting:
npm run build
npm run test:all
Integration tests may require local devnet/fork setup:
npm run test:integration
If not run, clearly report why.
Error Codes & Recovery
Map observed errors to actionable recovery:
| Error Class | Typical Trigger | Recovery Steps |
|---|---|---|
VALIDATION_ERROR | Amount.parse(...)/token mismatch, malformed address, invalid config | Confirm token decimals/symbol, re-create Amount from known token presets, validate config against src/types/* and src/sdk.ts. |
RPC_OR_NETWORK | RPC timeout, 429, transient JSON-RPC failures, chain mismatch | Retry with exponential backoff, check rpcUrl/chainId, verify provider health, reduce batch size for retries. |
TX_REVERTED | wallet.preflight(...) fails or receipt is reverted | Run wallet.preflight({ calls, feeMode }) first, inspect revert reason, reorder calls in wallet.tx(), split large multicalls. |
RATE_LIMIT_OR_TIMEOUT | tx.watch timeout, stalled polling, pool resolution timeout | Increase timeout where appropriate, add abort handling, retry on fresh provider session, avoid parallel heavy queries. |
AUTH_OR_PERMISSION | Privy signing errors, 401/403, invalid signature payloads | Verify signer server auth headers/body, validate trusted serverUrl, check examples/server/server.ts auth middleware alignment. |
UNDEPLOYED_ACCOUNT | wallet.execute(..., { feeMode: "user_pays" }) on undeployed account | Run wallet.ensureReady({ deploy: "if_needed" }), then retry execution; use sponsored mode only when paymaster path is configured. |
GENERATED_ASSET_DRIFT | Preset/docs changes diverge from source of truth | Regenerate via npm run generate:tokens, npm run generate:tokens:sepolia, npm run generate:validators, npm run generate:validators:sepolia, npm run docs:api, npm run docs:export. |
If a fix is uncertain:
- Reproduce with the closest example in
examples/*. - Capture command, environment, and failing test IDs.
- Report exact file/path + remediation attempted.
Useful Task Patterns
-
Bug fix in wallet lifecycle:
- inspect
src/wallet/index.ts,src/wallet/utils.ts - patch
- update
tests/wallet*.test.ts
- inspect
-
Privy auth/signature issue:
- inspect
src/signer/privy.ts - align with
examples/server/server.ts - update
tests/privy-signer.test.ts
- inspect
-
Staking regression:
- inspect
src/staking/staking.ts,src/staking/presets.ts - add/adjust integration assertions in
tests/integration/staking.test.ts
- inspect
Example Prompt
"Use this skill to fix Starkzap sponsored execution for undeployed accounts, add tests, and list behavior changes."