Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reference for building Starknet applications using starknet.js v9.x SDK, including contract interaction, account management, transaction handling, fee estimation, wallet integration, and paymaster flows.
.claude/skills/internet-court-starknet-js/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 102% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 123% | 0% |
Related modules: skills catalog.
bashnpm install starknet
Minimal setup to read from Starknet:
typescriptimport { RpcProvider, Contract } from 'starknet'; const provider = await RpcProvider.create({ nodeUrl: 'https://rpc.starknet.lava.build' }); const contract = new Contract(abi, contractAddress, provider); const result = await contract.get_balance();
Provider -> Account -> Contract
| | |
Network Identity InteractionUse Provider for read operations, Account for write operations.
typescriptimport { RpcProvider } from 'starknet'; // Recommended: Auto-detect RPC spec version const provider = await RpcProvider.create({ nodeUrl: 'https://rpc.starknet.lava.build' });
Networks:
https://rpc.starknet.lava.buildhttps://rpc.starknet-testnet.lava.buildKey Methods:
typescriptconst chainId = await provider.getChainId(); const block = await provider.getBlock('latest'); const nonce = await provider.getNonceForAddress(accountAddress); await provider.waitForTransaction(txHash); // Read storage directly const value = await provider.getStorageAt(contractAddress, storageKey);
Step 1: Compute address
typescriptimport { hash, ec, encode, CallData } from 'starknet'; // IMPORTANT: `stark.randomAddress()` returns an address-like random felt and is NOT a private key. // Use a real stark curve private key generator. const privateKey = '0x' + encode.buf2hex(ec.starkCurve.utils.randomPrivateKey()); const publicKey = ec.starkCurve.getStarkKey(privateKey); // NOTE: account class hashes are network/account-type dependent. // Treat this as an example only (verify the correct class hash for your setup). const classHash = '0x540d7f5ec7ecf317e68d48564934cb99259781b1ee3cedbbc37ec5337f8e688'; // example const constructorCalldata = CallData.compile({ publicKey }); const address = hash.calculateContractAddressFromHash(publicKey, classHash, constructorCalldata, 0);
Step 2: Fund the address with STRK before deployment.
Step 3: Deploy
typescriptimport { Account } from 'starknet'; // NOTE: Account constructor signature varies across starknet.js versions. // If this doesn't typecheck for your version, refer to the official docs. const account = new Account({ provider, address, signer: privateKey, cairoVersion: '1' }); const { transaction_hash } = await account.deployAccount({ classHash, constructorCalldata, addressSalt: publicKey }); await provider.waitForTransaction(transaction_hash);
Step 4: Use the account for transactions.
typescriptconst account = new Account({ provider, address: '0x123...', signer: privateKey, cairoVersion: '1' // Optional, auto-detected if omitted });
typescriptimport { Contract } from 'starknet'; const contract = new Contract(abi, contractAddress, provider); // Read-only const writeContract = new Contract(abi, contractAddress, account); // Read-write
typescript// Get full TypeScript autocomplete and type checking from ABI const typedContract = contract.typedv2(abi); const balance = await typedContract.balanceOf(userAddress);
typescriptconst balance = await contract.get_balance(); const userBalance = await contract.balanceOf(userAddress);
typescriptconst tx = await contract.increase_balance(100); await provider.waitForTransaction(tx.transaction_hash);
typescriptimport { CallData, cairo } from 'starknet'; const calls = [ { contractAddress: tokenAddress, entrypoint: 'approve', calldata: CallData.compile({ spender: bridgeAddress, amount: cairo.uint256(1000n) }) }, { contractAddress: bridgeAddress, entrypoint: 'deposit', calldata: CallData.compile({ amount: cairo.uint256(1000n) }) } ]; const tx = await account.execute(calls);
Using populate() for type-safety:
typescriptconst approveCall = tokenContract.populate('approve', { spender: bridgeAddress, amount: cairo.uint256(1000n) }); const depositCall = bridgeContract.populate('deposit', { amount: cairo.uint256(1000n) }); const tx = await account.execute([approveCall, depositCall]);
typescriptconst receipt = await provider.getTransactionReceipt(txHash); const events = contract.parseEvents(receipt); const transferEvents = contract.parseEvents(receipt, 'Transfer');
Simulate before executing to catch reverts and inspect state changes:
typescriptconst simResult = await account.simulateTransaction( [{ type: 'INVOKE', payload: calls }], { skipValidate: false } ); console.log('Fee estimate:', simResult[0].fee_estimation); console.log('Trace:', simResult[0].transaction_trace); // Check state changes before execution const trace = simResult[0].transaction_trace; if (trace?.state_diff) { console.log('Storage changes:', trace.state_diff.storage_diffs); }
typescriptconst fee = await account.estimateInvokeFee(calls); console.log({ overallFee: fee.overall_fee, resourceBounds: fee.resourceBounds // V3: l1_gas, l2_gas, l1_data_gas });
Execute with custom bounds:
typescriptconst tx = await account.execute(calls, { resourceBounds: { l1_gas: { amount: '0x2000', price: '0x1000000000' }, l2_gas: { amount: '0x0', price: '0x0' }, l1_data_gas: { amount: '0x1000', price: '0x1000000000' } } });
With priority tip:
typescriptconst tipStats = await provider.getEstimateTip(); const tx = await account.execute(calls, { tip: tipStats.percentile_75 });
typescriptconst receipt = await provider.waitForTransaction(txHash); // Status check helpers if (receipt.isSuccess()) { console.log('Transaction succeeded'); } else if (receipt.isReverted()) { console.log('Reverted:', receipt.revert_reason); } else if (receipt.isRejected()) { console.log('Rejected'); } else if (receipt.isError()) { console.log('Error'); }
Connect to browser wallets (ArgentX, Braavos):
typescriptimport { connect } from '@starknet-io/get-starknet'; import { WalletAccount } from 'starknet'; const selectedWallet = await connect({ modalMode: 'alwaysAsk' }); const walletAccount = await WalletAccount.connect( { nodeUrl: 'https://rpc.starknet.lava.build' }, selectedWallet ); // Use like regular Account const tx = await walletAccount.execute(calls); // Event handlers walletAccount.onAccountChange((accounts) => console.log('New account:', accounts[0])); walletAccount.onNetworkChanged((chainId) => console.log('Network changed:', chainId));
Setup paymaster for sponsored or alternative gas token transactions:
typescriptimport { PaymasterRpc, Account } from 'starknet'; const paymaster = new PaymasterRpc({ nodeUrl: 'https://sepolia.paymaster.avnu.fi' }); const account = new Account({ provider, address, signer: privateKey, paymaster });
Sponsored (dApp pays gas):
typescriptconst tx = await account.executePaymasterTransaction(calls, { feeMode: { mode: 'sponsored' } });
Alternative token (e.g., USDC):
typescriptconst tokens = await account.paymaster.getSupportedTokens(); const feeDetails = { feeMode: { mode: 'default', gasToken: USDC_ADDRESS } }; const estimate = await account.estimatePaymasterTransactionFee(calls, feeDetails); const tx = await account.executePaymasterTransaction(calls, feeDetails, estimate.suggested_max_fee_in_gas_token);
typescriptconst typedData = { types: { StarknetDomain: [ { name: 'name', type: 'shortstring' }, { name: 'version', type: 'shortstring' }, { name: 'chainId', type: 'shortstring' }, { name: 'revision', type: 'shortstring' } ], Message: [{ name: 'content', type: 'shortstring' }] }, primaryType: 'Message', domain: { name: 'MyDapp', version: '1', chainId: 'SN_SEPOLIA', revision: '1' }, message: { content: 'Hello Starknet' } }; const signature = await account.signMessage(typedData); const msgHash = await account.hashMessage(typedData); const isValid = ec.starkCurve.verify(signature, msgHash, publicKey);
typescriptimport { CallData, cairo, CairoCustomEnum, CairoOption, CairoOptionVariant } from 'starknet'; // Compile with ABI const calldata = new CallData(abi); const compiled = calldata.compile('transfer', { recipient: '0x...', amount: cairo.uint256(1000n) }); // Cairo type helpers - always use BigInt (n suffix) for token amounts cairo.uint256(1000n) // { low, high } - ALWAYS use BigInt for precision cairo.felt252(1000) // BigInt cairo.felt('0x123') // hex to felt cairo.bool(true) // Cairo bool cairo.byteArray('Hello') // ByteArray for long strings // Short strings (<= 31 chars) import { shortString } from 'starknet'; shortString.encodeShortString('hello') // felt252 shortString.decodeShortString('0x...') // 'hello' // Enums and Options const myEnum = new CairoCustomEnum({ Variant1: { value: 123 } }); const some = new CairoOption(CairoOptionVariant.Some, value);
Important: Always use BigInt (e.g., 1000n) for token amounts and balances. Never use Number() or parseFloat() on wei values -- JavaScript numbers lose precision above 2^53.
typescriptconst erc20 = new Contract(erc20Abi, tokenAddress, account); // Read balance (returns BigInt - do NOT convert with Number()) const balance = await erc20.balanceOf(account.address); console.log('Balance (wei):', balance.toString()); // Transfer (use BigInt for amount) const amount = cairo.uint256(1000000000000000000n); // 1 token (18 decimals) const tx = await erc20.transfer(recipientAddress, amount); await provider.waitForTransaction(tx.transaction_hash); // Approve + transferFrom pattern await erc20.approve(spenderAddress, cairo.uint256(amount));
typescriptimport { stark, ec, encode, num, hash } from 'starknet'; // Key generation const privateKey = '0x' + encode.buf2hex(ec.starkCurve.utils.randomPrivateKey()); const publicKey = ec.starkCurve.getStarkKey(privateKey); // Number conversions num.toHex(123); // '0x7b' num.toBigInt('0x7b'); // 123n // Hashing hash.getSelectorFromName('transfer'); hash.calculateContractAddressFromHash(salt, classHash, calldata, deployer);
typescript// Deploy via UDC const { transaction_hash, contract_address } = await account.deploy({ classHash: '0x...', constructorCalldata: CallData.compile({ owner: account.address }), salt: stark.randomAddress(), // random felt252 salt (not a private key) unique: true }); // Declare first, then deploy const declareResponse = await account.declare({ contract: compiledSierra, casm: compiledCasm }); await provider.waitForTransaction(declareResponse.transaction_hash); const deployResponse = await account.deploy({ classHash: declareResponse.class_hash, constructorCalldata: CallData.compile({ owner: account.address }) }); // Or combined const result = await account.declareAndDeploy({ contract: compiledContract, casm: compiledCasm, constructorCalldata: CallData.compile({ owner: account.address }) });
Execute transactions on behalf of another account (gasless/delegated):
typescriptconst version = await account.getSnip9Version(); // 'V1' | 'V2' | 'UNSUPPORTED' const outsideTransaction = await account.getOutsideTransaction( { caller: executorAddress, execute_after: now, execute_before: now + 3600 }, calls, 'V2' ); // Executor submits the pre-signed transaction const result = await executorAccount.executeFromOutside(outsideTransaction);
typescriptimport { LibraryError, RpcError } from 'starknet'; try { const tx = await account.execute(calls); } catch (error) { if (error instanceof RpcError) { console.error('RPC error:', error.code, error.message); } else if (error instanceof LibraryError) { console.error('Library error:', error.message); } }
typescriptimport { config, setLogLevel } from 'starknet'; // Global config config.set('transactionVersion', '0x3'); config.get('transactionVersion'); // Logging setLogLevel('DEBUG'); // ERROR | WARN | INFO | DEBUG
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,701 | 9,377 | -36% | 1 | 1 | 0% | 3,063 | 5,788 | +89% | 0 | 0 | — |
case-02 | fail→pass | 15,196 | 13,584 | -11% | 1 | 1 | 0% | 2,954 | 6,435 | +118% | 0 | 0 | — |
case-03 | fail→pass | 17,849 | 9,872 | -45% | 1 | 1 | 0% | 3,555 | 5,935 | +67% | 0 | 0 | — |
case-04 | fail→pass | 12,596 | 6,290 | -50% | 1 | 1 | 0% | 2,412 | 4,873 | +102% | 0 | 0 | — |
case-05 | pass→pass | 13,652 | 4,792 | -65% | 1 | 1 | 0% | 2,868 | 4,662 | +63% | 0 | 0 | — |
case-06 | fail→pass | 11,341 | 6,674 | -41% | 1 | 1 | 0% | 2,346 | 5,224 | +123% | 0 | 0 | — |
case-07 | fail→pass | 14,334 | 4,619 | -68% | 1 | 1 | 0% | 2,762 | 4,652 | +68% | 0 | 0 | — |
case-08 | fail→pass | 11,202 | 4,934 | -56% | 1 | 1 | 0% | 2,482 | 4,796 | +93% | 0 | 0 | — |
case-09 | fail→pass | 9,516 | 2,678 | -72% | 1 | 1 | 0% | 1,925 | 4,298 | +123% | 0 | 0 | — |
case-10 | fail→fail | 15,749 | 7,663 | -51% | 1 | 1 | 0% | 3,551 | 5,377 | +51% | 0 | 0 | — |
case-11 | fail→pass | 16,482 | 6,732 | -59% | 1 | 1 | 0% | 3,322 | 5,178 | +56% | 0 | 0 | — |
case-12 | fail→pass | 12,830 | 8,512 | -34% | 1 | 1 | 0% | 2,434 | 5,399 | +122% | 0 | 0 | — |
case-13 | pass→pass | 14,415 | 8,036 | -44% | 1 | 1 | 0% | 2,949 | 5,499 | +86% | 0 | 0 | — |
case-14 | fail→pass | 13,345 | 5,027 | -62% | 1 | 1 | 0% | 2,659 | 4,742 | +78% | 0 | 0 | — |
case-15 | pass→pass | 12,404 | 11,400 | -8% | 1 | 1 | 0% | 2,418 | 6,014 | +149% | 0 | 0 | — |
case-16 | fail→pass | 13,951 | 5,566 | -60% | 1 | 1 | 0% | 2,722 | 4,879 | +79% | 0 | 0 | — |
case-17 | fail→pass | 17,023 | 9,814 | -42% | 1 | 1 | 0% | 3,209 | 5,834 | +82% | 0 | 0 | — |
case-18 | pass→pass | 9,365 | 4,185 | -55% | 1 | 1 | 0% | 1,900 | 4,606 | +142% | 0 | 0 | — |
case-19 | fail→pass | 15,757 | 3,772 | -76% | 1 | 1 | 0% | 2,802 | 4,533 | +62% | 0 | 0 | — |
case-20 | pass→pass | 9,002 | 5,721 | -36% | 1 | 1 | 0% | 1,866 | 5,030 | +170% | 0 | 0 | — |
case-21 | pass→pass | 18,868 | 13,626 | -28% | 1 | 1 | 0% | 4,159 | 6,963 | +67% | 0 | 0 | — |
case-22 | fail→fail | 7,856 | 8,946 | +14% | 1 | 1 | 0% | 650 | 4,465 | +587% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.