Developers
TypeScript SDK
ShearClient: PDA derivation, unsigned instruction builders, finalized account reads and integer math matching the program.
@shear/sdk is the TypeScript client for the Shear program. Its ShearClient derives PDAs, builds unsigned instructions from the bundled Anchor IDL and reads finalized accounts. It never signs or sends a transaction; the caller does that with the owner’s key.
- ES module for Node.js.
loadIdl()readsidl/shear.jsonfrom the package withnode:fs. - Built on
@coral-xyz/anchor0.31.1,@solana/web3.js1.98.4 and@solana/spl-token0.4.15. - Amounts are
bigintin base units. See units.
Setup#
import anchor from '@coral-xyz/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { ShearClient } from '@shear/sdk';
const connection = new Connection(process.env.SOLANA_RPC_URL!, 'finalized');
const owner = Keypair.fromSecretKey(secretKey);
const client = new ShearClient(connection, new anchor.Wallet(owner));
const vault = client.vault();The constructor creates an Anchor Program with finalized commitment and confirmed preflight commitment. The wallet only fills the provider; the builders never use it to sign.
Constants#
| Export | Value | Meaning |
|---|---|---|
| SHEAR_PROGRAM_ID | GG8Db3XMAfAaa4RT38e5uaeFSDjztEyaATpR6B3AxMcn | Program ID. client.programId is read from the IDL address and matches it. |
| TSLA_X_MINT | XsDoVfqeBukxuZHWhdvWHBhgEHjGNst4MLodqsJHzoB | TSLAx mint, Token-2022. |
| USDC_MINT | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | USDC mint, legacy SPL Token. |
| PYTH_RECEIVER | rec2HHDDnjLfj4kE7VyEtFA1HPGQLK33259532cRyHp | Pyth receiver program. |
| TSLA_FEED | 16dad506d7db8da01c87581c87ca897a012a153557d4d578c3b9c9e1bc0632f1 | Pyth feed ID as a hex string. |
| TOKEN_PROGRAM_ID | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA | Re-exported from @solana/spl-token. |
| TOKEN_2022_PROGRAM_ID | TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb | Re-exported from @solana/spl-token. |
| SystemProgram | — | Re-exported from @solana/web3.js. |
| U64_MAX | 2^64 - 1 | Largest u64. |
| BPS | 10_000n | Basis-point denominator. |
| MULTIPLIER_SCALE | 1_000_000_000n | Multiplier scale. |
| PRICE_SCALE | 1_000_000n | USD mark scale. |
| UNSTAKE_SECONDS | 604_800 | Wait from requestUnstake to the earliest withdrawStake. |
| STAKE_DECIMALS | 6 | Decimals the program requires of the bidder stake mint. |
ShearClient#
| Member | Returns | Meaning |
|---|---|---|
| new ShearClient(connection, wallet, idl?) | ShearClient | idl defaults to loadIdl(). |
| program, programId, connection, wallet | readonly | Anchor program, ID from the IDL, and the constructor inputs. |
| pda(seed, ...keys) | PublicKey | PDA for a string seed followed by public keys or buffers. |
| vault(mint?) | PublicKey | ["vault", mint]. mint defaults to TSLA_X_MINT. |
| round(vault, index) | PublicKey | ["round", vault, u64le(index)]. index is a bigint. |
| position(vault, owner) | PublicKey | ["position", vault, owner]. |
| session(vault, close) | PublicKey | ["session", vault, i64le(close)]. close is a bigint of Unix seconds. |
| vaultPdas(vault?) | Record<string, PublicKey> | vault, selfProgram, programData, underlyingMint, usdcMint, shareMint, custody, premium, assetProgram, tokenProgram, systemProgram. |
| roundPdas(round) | Record<string, PublicKey> | round, collateral, deposits, reserve, bids, optionMint. |
| positionPdas(vault, owner, round?) | Record<string, PublicKey> | owner, position, positionShares, bidder; with round, also ticket and bid. |
| stakeConfig(vault) | PublicKey | ["stake-config", vault]. |
| stakePdas(vault, owner) | Record<string, PublicKey> | owner, bidder, escrow (["stake", bidder]) and stakeConfig. |
| instruction(name, args, accounts) | Promise<TransactionInstruction> | Unsigned instruction for a camelCase IDL name. |
| fetchAccount(address, expectedKind?) | Promise<DecodedAccount> | Finalized read. Checks the owner program, the discriminator, the data length and, if given, the account type. |
| fetchVault(address), fetchRound(address) | Promise<DecodedAccount> | fetchAccount with the type fixed. |
Module functions#
| Function | Returns | Meaning |
|---|---|---|
| loadIdl() | Idl | Bundled IDL, cached after the first read. |
| decodeAccount(data, idl?) | DecodedAccount | null | Matches the discriminator and returns { kind, data }, with kind such as Vault. Null for unknown data; throws on truncated data. |
| decodeEvent(base64, idl?) | { name, data } | null | Decodes the payload of one Program data: log line. |
| jsonValue(value) | unknown | Public keys to base58, BN and bigint to decimal strings, snake_case keys to camelCase. |
| u64le(value) | Buffer | 8-byte little-endian u64 for PDA seeds. |
Building instructions#
instruction(name, args, accounts) looks up name in the camelCase IDL, checks the argument count, converts bigint arguments to Anchor BN, and builds with accountsStrict. Anchor resolves no accounts, so the map must hold every account the instruction lists. Extra keys are ignored, which lets the helper maps be spread together.
Account names per instruction are under instruction accounts. The helpers do not derive the owner’s token accounts. TSLAx accounts use the Token-2022 program; USDC, share and call accounts use the legacy token program.
Deposit#
A position must exist before the first deposit; create it once with createPosition. The vault’s active flag decides which deposit instruction applies. See deposits.
| Vault state | Instruction | Shares |
|---|---|---|
active false, between rounds | fundReady(amount, minShares) | Minted at once at the custody ratio. |
active true, round in Auction, Active or Observed | queueDeposit(amount, minShares) | Minted by process_ticket after settlement, at the snapshot ratio. |
While the round is Disputed or Settled, both fail with WrongPhase. Deposits open again with fundReady once close_round has run.
import { Transaction, sendAndConfirmTransaction, type TransactionInstruction } from '@solana/web3.js';
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
import { TSLA_X_MINT, TOKEN_2022_PROGRAM_ID, depositShares } from '@shear/sdk';
const me = owner.publicKey;
const amount = 250_000_000n; // 2.5 TSLAx
const source = getAssociatedTokenAddressSync(TSLA_X_MINT, me, false, TOKEN_2022_PROGRAM_ID);
const ixs: TransactionInstruction[] = [];
if (!(await connection.getAccountInfo(client.position(vault, me)))) {
ixs.push(await client.instruction('createPosition', [], {
...client.vaultPdas(vault),
...client.positionPdas(vault, me),
}));
}
const { data: v } = await client.fetchVault(vault);
if (v.active === false) {
const pdas = client.vaultPdas(vault);
const custody = BigInt((await connection.getTokenAccountBalance(pdas.custody)).value.amount);
const supply = BigInt((await connection.getTokenSupply(pdas.shareMint)).value.amount);
const minShares = depositShares(amount, custody, supply); // fewer on chain fails with Slippage (6022)
ixs.push(await client.instruction('fundReady', [amount, minShares], {
...pdas,
...client.positionPdas(vault, me),
source,
}));
} else {
const round = client.round(vault, BigInt(v.roundIndex as string));
const minShares = 0n; // checked against the settlement snapshot; below it the deposit is refunded
ixs.push(await client.instruction('queueDeposit', [amount, minShares], {
...client.vaultPdas(vault),
...client.roundPdas(round),
...client.positionPdas(vault, me, round),
source,
}));
}
await sendAndConfirmTransaction(connection, new Transaction().add(...ixs), [owner]);Withdraw#
Exits follow the same split. redeemReady(shares, minAssets) pays at once while the vault is inactive. requestWithdrawal(shares) locks shares during a round; after settlement, process_ticket pays the ticket owner’s TSLAx account. Anyone can send it, and the keeper does.
// Vault inactive: burn shares, receive TSLAx now
await client.instruction('redeemReady', [shares, minAssets], {
...client.vaultPdas(vault),
...client.positionPdas(vault, me),
destination: source,
});
// Round running: queue on the ticket (the context also takes deposits and source)
await client.instruction('requestWithdrawal', [shares], {
...client.vaultPdas(vault),
...client.roundPdas(round),
...client.positionPdas(vault, me, round),
source,
});Bid#
Bidding needs a Bidder entry with allowed set by the vault admin, or an active bidder stake. A wallet places one bid per round, while the round is in Auction and before auctionEnd. See bidding.
- quantity
- Calls in 8-decimal call atoms.
100_000_000nis one call on one TSLAx. At most the round’soffered.
escrow = ceil(quantity × limitPremiumMicro / 10^8)
USDC moved into the round’s bid escrow byplaceBid. After the auction the bid pays ceil(allocation × clearing / 10^8) and claimBid refunds the rest.import { getAssociatedTokenAddressSync } from '@solana/spl-token';
import { USDC_MINT, mulDivCeil } from '@shear/sdk';
const { data: v } = await client.fetchVault(vault);
const round = client.round(vault, BigInt(v.roundIndex as string));
const usdc = getAssociatedTokenAddressSync(USDC_MINT, me);
const quantity = 1_500_000_000n; // 15 calls
const limitPremiumMicro = 4_250_000n; // 4.25 USDC per call
const escrow = mulDivCeil(quantity, limitPremiumMicro, 100_000_000n); // 63_750_000n = 63.75 USDC
const bid = await client.instruction('placeBid', [quantity, limitPremiumMicro], {
...client.vaultPdas(vault),
...client.roundPdas(round),
...client.positionPdas(vault, me, round),
source: usdc,
});Stake to bid#
stakeBidder locks the vault’s current stake amount from the wallet’s SHEAR account. requestUnstake starts the UNSTAKE_SECONDS wait; withdrawStake returns the stake once the wait has passed and every bid placed through it is claimed.
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
const config = await client.fetchAccount(client.stakeConfig(vault), 'stakeConfig');
const mint = new PublicKey(config.data.mint as string);
const shear = getAssociatedTokenAddressSync(mint, me);
const stake = client.stakePdas(vault, me);
const lock = await client.instruction('stakeBidder', [], { ...client.vaultPdas(vault), ...stake, mint, source: shear });
const ask = await client.instruction('requestUnstake', [], { vault, ...stake });
// Seven days later, with every staked bid claimed:
const back = await client.instruction('withdrawStake', [], { ...client.vaultPdas(vault), ...stake, destination: shear });Claim#
| Instruction | Signer | When | Pays |
|---|---|---|---|
| claimBid | Bidder | Round past Auction. | Unused escrow in USDC and the allocated calls. |
| claimPremium | Share owner | Any time. | All credited premium in USDC. |
| claimOption | Call holder | Round Settled or Closed. | TSLAx for the calls burned. |
The call token account must exist before claimBid. Calls use the legacy token program, so a standard associated token account works.
import { createAssociatedTokenAccountIdempotentInstruction, getAssociatedTokenAddressSync } from '@solana/spl-token';
import { TSLA_X_MINT, TOKEN_2022_PROGRAM_ID, USDC_MINT } from '@shear/sdk';
const { optionMint } = client.roundPdas(round);
const options = getAssociatedTokenAddressSync(optionMint, me);
const usdc = getAssociatedTokenAddressSync(USDC_MINT, me);
const tslax = getAssociatedTokenAddressSync(TSLA_X_MINT, me, false, TOKEN_2022_PROGRAM_ID);
const accounts = {
...client.vaultPdas(vault),
...client.roundPdas(round),
...client.positionPdas(vault, me, round),
};
// Bidder: refund and calls, once the round has left Auction
const claimBid = [
createAssociatedTokenAccountIdempotentInstruction(me, options, me, optionMint),
await client.instruction('claimBid', [], { ...accounts, refund: usdc, options }),
];
// Share owner: all credited premium
const claimPremium = await client.instruction('claimPremium', [], { ...accounts, destination: usdc });
// Call holder: burn calls for TSLAx once the round is Settled
const calls = BigInt((await connection.getTokenAccountBalance(options)).value.amount);
const claimOption = await client.instruction('claimOption', [calls], { ...accounts, options, destination: tslax });A call claim pays payout(claimed + quantity) - payout(claimed) against the whole round, so it can differ from coveredPayout for the same quantity by one atom. After a fallback settlement each call atom pays one TSLAx atom. See calls and premium.
Reading state#
fetchAccount reads at finalized commitment and returns decoded data with camelCase keys. 64-bit and wider integers arrive as decimal strings, so convert them with BigInt. The phase is an object with one key.
const { data: v } = await client.fetchVault(vault);
const index = BigInt(v.roundIndex as string); // 0n before the first round
if (index > 0n) {
const { data: r } = await client.fetchRound(client.round(vault, index));
const phase = Object.keys(r.phase as object)[0]; // 'Auction' | 'Active' | 'Observed' | 'Settled' | 'Closed' | 'Disputed'
const strike = BigInt(r.strike as string); // micro-USD per whole TSLAx
const auctionEnd = Number(r.auctionEnd as string); // Unix seconds
}
const { data: p } = await client.fetchAccount(client.position(vault, me), 'position');
const claimable = // micro-USDC
BigInt(p.premiumCredit as string) +
(BigInt(p.shares as string) * (BigInt(v.premiumIndex as string) - BigInt(p.premiumIndex as string)) +
BigInt(p.premiumFraction as string)) / 10n ** 18n;Token balances of custody, collateral, reserve, the share mint and the call mint are ordinary SPL reads on the addresses from vaultPdas and roundPdas. For many accounts at once, or for event history, use the indexer API.
Events#
decodeEvent decodes the base64 payload of one Program data: line. It does not check which program wrote the line, and a CPI can log bytes shaped like a Shear event. Decode only lines written while Shear is the active program, from successful transactions, as the indexer does.
import { SHEAR_PROGRAM_ID, decodeEvent } from '@shear/sdk';
const tx = await connection.getTransaction(signature, { commitment: 'finalized', maxSupportedTransactionVersion: 0 });
const stack: string[] = [];
if (tx?.meta && tx.meta.err === null) {
for (const line of tx.meta.logMessages ?? []) {
const invoke = /^Program (\S+) invoke \[\d+\]$/.exec(line);
if (invoke) { stack.push(invoke[1]!); continue; }
if (/^Program \S+ (success|failed:.*)$/.test(line)) { stack.pop(); continue; }
if (stack.at(-1) === SHEAR_PROGRAM_ID.toBase58() && line.startsWith('Program data: ')) {
const event = decodeEvent(line.slice('Program data: '.length));
if (event) console.log(event.name, event.data); // e.g. BidPlaced { round, owner, quantity, limit }
}
}
}Math helpers#
Pure bigint functions that follow the program’s integer rounding. They throw RangeError for negative inputs, zero denominators and results outside u64.
| Function | Rule |
|---|---|
| u64(value) | Returns value if it fits in u64; throws otherwise. |
| mulDivFloor(a, b, d) | floor(a × b / d). |
| mulDivCeil(a, b, d) | ceil(a × b / d). |
| coveredPayout(amount, price, multiplier, strikeRawPrice) | TSLAx atoms owed on amount call atoms. Raw price = floor(price × multiplier / 10^9); 0 if the raw price is at or below the strike, else floor(amount × (raw - strike) / raw). price is the 6-decimal Pyth mark. |
| depositShares(amount, assets, supply) | amount for an empty vault, else floor(amount × supply / assets). Throws for a zero result or zero assets. |
| clearAuction(capacity, reserve, bids) | Uniform-price clearing as on chain. Skips bids below reserve or with zero quantity; at most 16 bids. |
clearAuction takes { quantity, limitPremium, owner } per bid, with owner as PublicKey.toBytes() for tie-breaking. It returns { clearingPremium, sold, allocations }, allocations in input order. Charges are not returned; each is mulDivCeil(allocation, clearingPremium, 100_000_000n). The SDK has no strike helper: on chain the strike is ceil(effective price × (10000 + otm_bps) / 10000).
import { clearAuction, coveredPayout, MULTIPLIER_SCALE } from '@shear/sdk';
const result = clearAuction(12_050_000_000n, 1_000_000n, [
{ quantity: 8_000_000_000n, limitPremium: 4_250_000n, owner: bidderA.toBytes() },
{ quantity: 6_000_000_000n, limitPremium: 3_900_000n, owner: bidderB.toBytes() },
]);
// { clearingPremium: 3_900_000n, sold: 12_050_000_000n, allocations: [8_000_000_000n, 4_050_000_000n] }
// 10 calls, strike 262.629629 USD, close mark 280 USD, multiplier 1.0
coveredPayout(1_000_000_000n, 280_000_000n, MULTIPLIER_SCALE, 262_629_629n);
// 62_037_039n TSLAx atoms, 0.62037039 TSLAx