covr lets people earn on JitoSOL by selling covered calls. A holder picks a sell price (the strike) and a date. They lock their JitoSOL and get paid USDC upfront. Market makers buy those calls.
These docs are for desks that want to buy those calls. They cover:
A customer asks for a quote: sell 1 JitoSOL at $180 by 30 October.
covr's gateway sends that request (an RFQ) to every connected desk.
Desks send a sealed, signed bid within 3 seconds. The highest price wins.
The winner signs an executable quote. The customer signs the fill on Solana.
The program takes the premium from the desk's USDC vault, pays it to the customer minus covr's fee, and locks the customer's JitoSOL.
At expiry the desk decides. If JitoSOL is above the strike, the desk exercises: it pays the strike in USDC and receives the JitoSOL. If not, the JitoSOL goes back to the customer.
On each position the most a desk can lose is the premium it paid. It needs the premium in its vault at fill and the strike at exercise. Capital and settlement has the full money flow.
The SDK builds instructions, signs gateway messages and speaks the gateway's WebSocket protocol. It does not price, hold keys, pick an RPC or submit transactions. Those stay with you.
Every function wraps covr's own reviewed code. covr's tests check that each builder emits the same bytes as covr's own builders. They also check that each signed message passes the gateway's verifiers. The package ships golden vectors, so you can check your own signer the same way.
covr turns it on per desk and issues a bearer token.
Winning customer orders
Not yet. Until covr lists your desk on its markets, your bids are fully verified and then refused with Bid intent verified but not entered: this market does not list this maker yet. We call this practice mode.
Assets
JitoSOL. Tokenized-stock markets stay covr-only for now.
Markets you create
Valid on chain, but not shown to customers yet.
Stopping
Pause your mint policy, or raise min_nonce on chain. There is no gateway withdrawal for desks.
Go from nothing to watching real RFQs arrive. You register your maker on chain, get a gateway token from covr, and run the example bot in dry-run mode. Dry run signs bids locally and sends nothing.
Time: about 30 minutes, plus covr's reply with your token.
Before your keys sign anything, prove your signing stack matches covr's. The package ships signed test vectors made with a public throwaway key. Digests must match exactly, and each signature must recover to the vector key. Custom signers has a script that checks both.
Registration is one transaction. Your quote key signs a grant and your authority sends it. You need three small files.
keys.ts loads your two keys:
ts
import { readFile } from'node:fs/promises';
import { Wallet } from'ethers';
import { createKeyPairSignerFromBytes } from'@solana/kit';
// Your Solana authority: a keypair JSON file in the solana-keygen format.exportconst authority = awaitcreateKeyPairSignerFromBytes(
Uint8Array.from(JSON.parse(awaitreadFile(process.env.AUTHORITY_KEYPAIR!, 'utf8'))),
);
// Your quote key. A Wallet keeps this short. In production, use a KMS adapter (see Custom signers).exportconst quoteSigner = newWallet(process.env.MAKER_QUOTE_KEY!);
send.ts signs instructions with your authority and waits for finalized:
ts
// send.ts: one helper the guides reuse. Your authority pays fees and signs.import {
appendTransactionMessageInstructions, createSolanaRpc, createSolanaRpcSubscriptions,
createTransactionMessage, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners, assertIsTransactionWithBlockhashLifetime,
typeInstruction, typeKeyPairSigner,
} from'@solana/kit';
const rpc = createSolanaRpc(process.env.RPC_URL!);
const rpcSubscriptions = createSolanaRpcSubscriptions(process.env.RPC_WS_URL!);
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
exportasyncfunctionsend(authority: KeyPairSigner, ...instructions: Instruction[]) {
const { value: blockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
const message = pipe(
createTransactionMessage({ version: 0 }),
m =>setTransactionMessageFeePayerSigner(authority, m),
m =>setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
m =>appendTransactionMessageInstructions(instructions, m),
);
const tx = awaitsignTransactionMessageWithSigners(message);
assertIsTransactionWithBlockhashLifetime(tx);
awaitsendAndConfirm(tx, { commitment: 'finalized' });
returngetSignatureFromTransaction(tx);
}
register.ts registers your maker:
ts
import { signRegistrationGrant, registerMakerV3 } from'@covrfun/maker-sdk';
import { authority, quoteSigner } from'./keys.ts';
import { send } from'./send.ts';
const makerEthAddress = quoteSigner.address;
// The quote key grants your authority the right to register it. Checked locally before sending.const grant = awaitsignRegistrationGrant(quoteSigner, authority.address);
const { maker, instruction } = awaitregisterMakerV3({
authority: authority.address, makerEthAddress, signature: grant.signature,
});
console.log('registered', { maker, makerEthAddress, authority: authority.address }, awaitsend(authority, instruction));
Every covr order is a short sealed-bid auction. A customer asks for a price. Desks bid. The best bid wins and signs a quote the customer can fill on chain.
When the gateway accepts your quote it replies "OK". That only means the customer can now see it. The customer may never sign. No fill message follows. A fill shows up only on chain, as your position account. See Fills and exercise.
Until covr lists your desk on its markets, the gateway checks your bids and then refuses them with:
text
Bid intent verified but not entered: this market does not list this maker yet
Practice mode proves
Practice mode does not test
Your token, connection and heartbeats
The 3 s bid deadline and the one-bid rule
Reading RFQs and market accounts
Awards, quotes and award declines
Your bid-intent signature, terms and asset
Your nonce journal
Fills, reconciliation and exercise
Test the parts practice mode cannot reach yourself: replay awards against your own code, and rehearse exercise with the exercise guide. See also Gateway: practice mode.
Your desk holds USDC in one vault on chain. The program takes the premium from it when an order fills. It takes the strike from it only if you exercise. Nothing is escrowed per position.
Gross premium leaves your vault. The customer receives it minus the market fee. The fee goes to the market's fee owner. The customer's JitoSOL is locked in a position account.
Exercise (optional)
You (authority or operator)
Inside the exercise window: the strike leaves your vault and goes to the customer. The locked JitoSOL goes to your authority's token account.
Release
Anyone
After the window, if you did not exercise: the JitoSOL goes back to the customer. Your exposure and usage are freed.
All amounts are integers in atoms, a token's smallest unit. USDC has 6 decimals (1 USDC = 10^6 atoms). JitoSOL has 9 (1 JitoSOL = 10^9 atoms).
Amount
Formula
SDK
Gross premium
atoms × priceE18 / 10^(decimals + 12)
grossPremium
Fee
floor(gross × feeBps / 10000), feeBps at most 1,000
fee
Customer receives
gross − fee
writerPremium
Strike payment
atoms × strikeE8 / 10^(decimals + 2)
strikePayment
priceE18 is USDC per one whole token, scaled by 10^18. strikeE8 is USD per token, scaled by 10^8. Gross premium and strike payment must both come out as whole USDC atoms. If they don't, the program refuses the fill. Use exactPriceAtOrBelow to round your price down to one that pays exactly.
That fill's gross premium. Otherwise the fill fails.
Each exercise
That position's strike payment. Otherwise the exercise fails.
The program does not set the strike aside when a position opens. You decide how much strike to cover:
Fully funded. Keep every open strike in the vault. Simple and safe.
Just in time. Keep premiums in the vault and top it up before you exercise. Positions on one market all expire together, and the exercise window is 60 to 3,600 seconds, so plan the top-up ahead.
Warning
If the vault cannot pay the strike, the exercise fails. If you still have not exercised when the window closes, anyone can release the JitoSOL back to the customer. You lose the premium and the position's value.
Six on-chain limits cap your risk. You set all of them with your authority.
Limit
Scope
Set with
Exposure
Open JitoSOL atoms across all markets
initializeExposureV3
Max order size
JitoSOL atoms in one fill
initializeMintPolicyV1 (maxOrderAtoms)
Reserved collateral
Open JitoSOL atoms under this policy
initializeMintPolicyV1 (maxReservedAtoms)
Reserved strike
Open strike obligations, in USDC atoms
initializeMintPolicyV1 (maxReservedStrikeUsdc)
Mint lifetime premium
Lifetime premium paid on this mint, in USDC atoms. Optional.
mintPolicySettingsFor (lifetimePremiumLimitUsdc)
Premium budget
Lifetime gross premium paid across all mints, in USDC atoms. Optional. Disabled means no cap. It never resets.
setPremiumBudget
A fill that would break any limit fails on chain. Change the exposure limit later with setExposureLimit, and the mint policy with updateMintPolicyV1.
The program adds one limit of its own: one JitoSOL fill can be worth at most 2 SOL of stake-pool value. On 15 October 2026 the pool rate was 1.306 SOL per JitoSOL, so the cap was 1.53 JitoSOL. It shrinks as JitoSOL earns staking yield. Larger fills fail with JitoSolOrderLimit.
The quote key never touches SOL or USDC directly. The authority owns your accounts and your vault. You can also name an operator: a second Solana key that may exercise for you without holding the authority.
Every message is EIP-712 typed data. The domain is:
Field
Value
name
SOL Covered Calls
version
1
chainId
2000000001 (CHAIN_ID)
verifyingContract
Derived from the market's full terms (quoteDomain)
The verifyingContract commits to every term of the market: program, deployment, collateral, strike, expiry, fee and more. So a bid or quote signed for one market can never be replayed on another. Always build the domain from the market account you read yourself:
The program rejects a signature whose s value is in the upper half of the curve order. signIntent, signQuote and signRegistrationGrant check this for you. If you sign another way, run assertCanonicalSignature on the result. Most libraries already produce low-s signatures.
Every quote carries a nonce: an unsigned 64-bit integer you choose. The nonce names the position the quote can open. Your position account address is derived from your quote key and that nonce. So each nonce can open at most one position, ever, across all markets.
The program accepts a quote only when:
text
nonce >= min_nonce and nonce != 2^64 - 1
min_nonce lives in your Maker account. Read it with decodeMaker.
A quote request that times out may still have reached the gateway. The same holds for a socket that dies while sending it. The SDK reports these as OutcomeUnknownError, never as "not sent".
Treat that quote as live until one of these is true:
your position account for that nonce exists (it filled), or
its validUntil has passed and no fill landed.
Keep its cash held until then. The SDK also refuses any further request for that RFQ, so a late reply cannot be mistaken for a new one.
The gateway has no withdrawal route for desks. You stop quotes on chain, two ways:
Pause your mint policy. Every fill on that mint fails until you re-enable it. Your authority or a hot watcher key can do it.
Raise min_nonce with configureMaker. Every quote below the new floor can never fill. The floor never goes down.
configureMaker sets bothoperator and minNonce. Pass your current operator to keep it. See Cancel quotes.
Tip
A quote the gateway admits is valid for at most 60 seconds. The brakes matter when you must stop now: a bad price or a market move. A leaked quote key needs more: see Security.
USD per token, scaled by 10^8. 18_000_000_000n is $180.
Expiry
Unix seconds. All positions on a market expire together.
Window
Seconds after expiry when the buyer may exercise: 60 to 3,600.
Max quantity
The most collateral atoms one fill may carry. The program also caps every JitoSOL fill at 2 SOL of stake-pool value, about 1.53 JitoSOL in October 2026.
Fee (feeBps)
covr's cut of the gross premium, at most 1,000 (10%).
Paused
A paused market accepts no new fills.
Settlement mode
0 ordinary: exercise is one atomic transaction. 1 restricted: tokenized stocks, covr-only for now.
decodeMarketV3 checks the layout, the mainnet genesis hash, USDC as payment, covr's deployment id, and that the address is the right PDA for its creator and salt.
There is no market list for desks. You learn about a market from the RFQ that names it.
RFQ quantities arrive as decimal strings scaled to 18 decimals. Convert with atomsFromWire(quantity, decimals). Each quantity is a multiple of the market's minimum lot (minimumExactLot), so the strike payment is always a whole USDC atom.
Save as setup.ts and run it with npx tsx setup.ts. The amounts are examples. Each step creates an account once: if a later step fails, comment out the steps that already landed before you run it again. Pick your own limits. All amounts are in atoms, the token's smallest unit: 1 JitoSOL is 10^9 atoms and 1 USDC is 10^6 atoms.
ts
import {
JITOSOL_MINT, PROGRAM_ID, TOKEN_PROGRAM, USDC_MINT,
initializeVault, initializeExposureV3, decodeMarketV3, mintPolicySettingsFor,
initializeMintPolicyV1, bootstrapMintUsageV1, setPremiumBudget, deposit, decodeMaker, deriveMaker,
} from'@covrfun/maker-sdk';
import { createSolanaRpc, address } from'@solana/kit';
import { findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstructionAsync } from'@solana-program/token';
import { authority, quoteSigner } from'./keys.ts';
import { send } from'./send.ts';
const rpc = createSolanaRpc(process.env.RPC_URL!);
const makerEthAddress = quoteSigner.address;
const anyJitoSolMarket = process.env.MARKET!; // a covr JitoSOL market, from an RFQ in your dry runconst watcher = process.env.WATCHER; // a hot key that may pause your mint policyif (!watcher) throwError('WATCHER is required: a hot key that can pause this policy (changing it later needs an authority-signed updateMintPolicyV1)');
const [usdcSource] = awaitfindAssociatedTokenPda({ // your authority's USDC accountowner: authority.address, mint: address(USDC_MINT), tokenProgram: address(TOKEN_PROGRAM),
});
const { maker } = awaitderiveMaker(makerEthAddress);
const jitoSol = { mint: JITOSOL_MINT, program: TOKEN_PROGRAM, decimals: 9 };
// 2. USDC vault. Its premium budget starts disabled, which means no cap.const { vault, instruction: openVault } = awaitinitializeVault({ authority: authority.address, maker });
awaitsend(authority, openVault);
// 3. Exposure: cap on open JitoSOL atoms across all markets (here 5 JitoSOL).const { instruction: exposure } = awaitinitializeExposureV3({
authority: authority.address, makerEthAddress, collateral: jitoSol, limitAtoms: 5_000_000_000n,
});
awaitsend(authority, exposure);
// 4. Mint policy, bound to the terms of any JitoSOL market.const acct = await rpc.getAccountInfo(address(anyJitoSolMarket), { commitment: 'finalized', encoding: 'base64' }).send();
if (!acct.value || acct.value.owner !== PROGRAM_ID) throwError('not a covr market');
const market = awaitdecodeMarketV3(anyJitoSolMarket, Buffer.from(acct.value.data[0], 'base64'));
const settings = mintPolicySettingsFor(market, {
watcher,
maxOrderAtoms: 1_000_000_000n, // 1 JitoSOL per fillmaxReservedAtoms: 5_000_000_000n, // 5 JitoSOL open at oncemaxReservedStrikeUsdc: 1_000_000_000n, // 1,000 USDC of strikes open at once
});
const { instruction: policy } = awaitinitializeMintPolicyV1({
authority: authority.address, maker, collateral: JITOSOL_MINT, settings,
});
awaitsend(authority, policy);
// 5. Bootstrap usage. Fills on this mint fail until this runs.const { instruction: bootstrap } = awaitbootstrapMintUsageV1({
authority: authority.address, makerEthAddress, collateral: JITOSOL_MINT,
});
awaitsend(authority, bootstrap);
// 6. Optional: lifetime cap on gross premium paid (here 500 USDC).const { instruction: budget } = awaitsetPremiumBudget({
authority: authority.address, maker, enabled: true, limit: 500_000_000n,
});
awaitsend(authority, budget);
// 7. Fund the vault (here 1,000 USDC).const { instruction: fund } = awaitdeposit({ authority: authority.address, maker, source: usdcSource, amount: 1_000_000_000n });
awaitsend(authority, fund);
// 8. Your JitoSOL token account, owned by the authority. Exercise delivers here.awaitsend(authority, awaitgetCreateAssociatedTokenIdempotentInstructionAsync({
payer: authority, owner: authority.address, mint: address(JITOSOL_MINT),
}));
// Check: read your Maker account back.const makerAcct = await rpc.getAccountInfo(maker, { commitment: 'finalized', encoding: 'base64' }).send();
if (!makerAcct.value || makerAcct.value.owner !== PROGRAM_ID) throwError('maker account missing');
const decoded = awaitdecodeMaker(maker, Buffer.from(makerAcct.value.data[0], 'base64'), makerEthAddress);
console.log({ maker, vault, minNonce: decoded.minNonce, authority: decoded.authority });
The package ships a complete example maker. It listens to the JitoSOL feed, bids, answers awards, journals every quote and reconciles fills on chain. Use it to learn the flow, then build your own or harden it.
Warning
It is an example, not a production maker. It has no inventory, cash or risk limits beyond what the program enforces, and its price is a stub.
Until covr lists your desk, each bid comes back as:
text
Bid intent verified but not entered: this market does not list this maker yet
That means your token, signature, terms and asset all passed. Practice mode stops there: it never tests the bid deadline, awards, quotes, your journal or exercise. See Practice mode. Once covr lists your desk, the same bot starts winning.
In live mode the bot writes one JSON line per event: reserve, signed, sent, settled. On restart it reads the journal back, lists every open quote and never reuses a nonce. A line it cannot read stops it.
It also takes <NONCE_JOURNAL>.lock. A second live bot on the same journal refuses to start. A clean stop (Ctrl-C or SIGTERM) removes the lock. After a crash the lock stays on purpose: make sure no other bot uses the journal, then delete it.
The bot reconnects with capped exponential backoff and jitter: 1 second, doubling to 30. Bids from the old connection are dropped. Signed quotes keep being checked on chain.
The gateway never tells you about a fill. You find fills on chain. Then, at expiry, you exercise the positions that are worth it before the window closes.
For every quote in your journal, read that address at finalized commitment:
What you see
Meaning
The account exists, owned by the covr program
Filled. Track it to expiry.
No account, the quote's validUntil has passed, and no transaction ever referenced the address
Not filled. Release the cash you held for it.
Anything else
Unknown. Keep the cash held and check again.
Warning
A missing account alone does not prove "not filled". A position can fill and later close. Check the address's transaction history too (getSignaturesForAddress), from a node that has reached the slot you read.
Read the position and your Maker account in onegetMultipleAccounts call, so both come from the same slot. decodePositionV3 trusts the slot you pass; it cannot tell if the two reads came from different slots.
Exercising pays the strike and delivers the JitoSOL, so it only makes sense when JitoSOL is worth more than the strike. The premium is already spent either way. The decision is yours; the rules are fixed by the market:
Rule
Value
Opens
At the market's expiry (chain time)
Closes
expiry + window. The window is 60 to 3,600 seconds.
Who signs
Your authority or your operator
Pays
The strike, from your vault, to the customer
Delivers
The locked JitoSOL, to a token account owned by your authority
Every position on a market expires at the same moment. Fund the vault for all strikes you plan to pay before expiry, and leave time to land every transaction inside the window.
The exercise pays the customer into their token accounts. The program refuses a customer account that someone else could control: one with a delegate, or with a close authority other than the customer. The customer controls their own accounts and could change or close one to block your exercise. So the exercise creates fresh token accounts for the customer, owned by the customer, in the same transaction. covr's own maker does the same.
exerciseV3Instructions returns the whole transaction body, in order:
a compute-unit limit of 400,000 (the default 200,000 is not enough);
two fresh customer token accounts (USDC and JitoSOL);
exercise_v3, paying the strike from your vault and sending the JitoSOL to your authority's token account (setup step 8).
Do not add a second compute-unit limit: the transaction would be refused. You may add a priority fee.
The job signs with one key: your authority or your operator. It needs no quote key. Save this as signer.ts next to send.ts:
ts
import { readFile } from'node:fs/promises';
import { createKeyPairSignerFromBytes } from'@solana/kit';
// The one key this job signs with: your authority, your operator (exercise) or your watcher (pause).// It needs no quote key, so an exercise or pause box never holds one.exportconst signer = awaitcreateKeyPairSignerFromBytes(
Uint8Array.from(JSON.parse(awaitreadFile(process.env.SIGNER_KEYPAIR!, 'utf8'))),
);
Then exercise.ts:
ts
import {
PROGRAM_ID, WRITER_ACCOUNT_SPACE,
deriveMaker, derivePosition, decodeMarketV3, decodePositionV3, exerciseV3Instructions,
} from'@covrfun/maker-sdk';
import { address, createSolanaRpc } from'@solana/kit';
import { signer } from'./signer.ts'; // your authority or your operatorimport { send } from'./send.ts';
const rpc = createSolanaRpc(process.env.RPC_URL!);
const makerEthAddress = process.env.MAKER_ETH_ADDRESS!; // your quote key's 0x addressconst makerAuthority = process.env.MAKER_AUTHORITY!; // your authority's address, pinned in configconst nonce = BigInt(process.env.NONCE!); // this position's nonce, from your journalconst marketAddress = process.env.POSITION_MARKET!; // this position's market, from your journalconstb64 = (data: readonly [string, string]) => Buffer.from(data[0], 'base64');
// 1. Read the market, then the position and your Maker account in ONE finalized call.const m = await rpc.getAccountInfo(address(marketAddress), { commitment: 'finalized', encoding: 'base64' }).send();
if (!m.value || m.value.owner !== PROGRAM_ID) throwError('not a covr market');
const market = awaitdecodeMarketV3(marketAddress, b64(m.value.data));
const { maker } = awaitderiveMaker(makerEthAddress);
const { position } = awaitderivePosition(makerEthAddress, nonce, market.collateral);
const { context, value } = await rpc
.getMultipleAccounts([position, maker], { commitment: 'finalized', encoding: 'base64' })
.send();
const [posAcct, makerAcct] = value;
if (!posAcct || !makerAcct || posAcct.owner !== PROGRAM_ID || makerAcct.owner !== PROGRAM_ID)
throwError('position not found: this quote did not fill, or the position is closed');
const decoded = awaitdecodePositionV3({
position: { address: position, data: b64(posAcct.data) },
maker: { address: maker, data: b64(makerAcct.data) },
slot: context.slot, makerEthAddress, makerAuthority, nonce, collateral: market,
});
if (decoded.status !== 'open') throwError(`position is ${decoded.status}`);
// 2. One transaction: compute limit, two fresh customer token accounts, exercise_v3.// The JitoSOL goes to your authority's token account (setup step 8) by default.const rent = await rpc.getMinimumBalanceForRentExemption(BigInt(WRITER_ACCOUNT_SPACE), { commitment: 'finalized' }).send();
const { instructions } = awaitexerciseV3Instructions({
market, position: decoded, authority: signer.address, rentLamports: rent,
});
console.log('exercised', awaitsend(signer, ...instructions));
Run it once per position, with that position's market and nonce from your journal:
Read getMinimumBalanceForRentExemption(165) from your RPC. Rent can change, so read it each time.
makerCollateral
Optional. Defaults to your authority's JitoSOL account. It must exist (setup step 8) and must be owned by the authority, even when your operator signs.
With a priority fee added, the transaction is 1,111 bytes at most when the signer also pays the fee, within Solana's 1,232-byte limit. With a separate fee payer it is 1,207 bytes, which leaves no room for a Jito tip.
Each exercise funds two fresh customer token accounts: about 0.003 SOL at current rent, which the customer, not you, can later reclaim.
If you do not exercise, nothing more is needed from you. After the window closes, anyone can call release_v3. The JitoSOL goes back to the customer, and your exposure and mint usage are freed for new orders. The customer, covr or you can send it.
To free your limits sooner, send it yourself with releaseV3Instructions. Like exercise, it creates a fresh customer-owned JitoSOL account in the same transaction, and sets a 400,000 compute-unit limit.
The pause is fast and reversible. It can be signed by a watcher: a hot key you name in the policy settings, so your cold authority does not need to be online. Set one with mintPolicySettingsFor(market, { watcher, … }) at setup, or later with an authority-signed updateMintPolicyV1.
Read the policy's current generation first: the pause applies only to the version you read. If another update lands first, re-read and resend.
It signs with one key, your watcher or your authority, using signer.ts from Fills and exercise. Save as pause.ts:
ts
import { JITOSOL_MINT, PROGRAM_ID, TOKEN_PROGRAM, decodeMintPolicy, deriveMaker, deriveMintPolicy, pauseMintPolicyV1 } from'@covrfun/maker-sdk';
import { createSolanaRpc } from'@solana/kit';
import { signer } from'./signer.ts'; // your watcher (or your authority)import { send } from'./send.ts';
const rpc = createSolanaRpc(process.env.RPC_URL!);
const makerEthAddress = process.env.MAKER_ETH_ADDRESS!; // your quote key's 0x addressconst makerAuthority = process.env.MAKER_AUTHORITY!; // your authority's addressconst { maker } = awaitderiveMaker(makerEthAddress);
const { policy } = awaitderiveMintPolicy(maker, JITOSOL_MINT);
// Read the policy's current generation: the pause applies only to the version you read.// 'confirmed' is fine here: a stale generation only fails with MintPolicyGeneration (6012).const { value } = await rpc.getAccountInfo(policy, { commitment: 'confirmed', encoding: 'base64' }).send();
if (!value || value.owner !== PROGRAM_ID) throwError('mint policy missing');
const current = awaitdecodeMintPolicy(policy, Buffer.from(value.data[0], 'base64'), {
authority: makerAuthority, maker,
collateral: { mint: JITOSOL_MINT, program: TOKEN_PROGRAM, decimals: 9 },
});
const { instruction } = awaitpauseMintPolicyV1({
signer: signer.address, maker, collateral: JITOSOL_MINT, expectedGeneration: current.generation,
});
console.log('paused', awaitsend(signer, instruction));
Raising min_nonce cancels every quote below the new floor, permanently. Only your authority can do it.
Stop your bot (or pause the policy) first. Otherwise a quote signed while you raise the floor can land above it and stay live. Save this as cancel.ts next to keys.ts and send.ts, and run it with NEXT_NONCE=<next nonce from your journal> plus your usual key and RPC variables. configureMaker sets both the operator and the floor, so read the current operator first and pass it back.
ts
import { PROGRAM_ID, configureMaker, decodeMaker, deriveMaker } from'@covrfun/maker-sdk';
import { createSolanaRpc } from'@solana/kit';
import { authority, quoteSigner } from'./keys.ts'; // only the authority can call configureMakerimport { send } from'./send.ts';
// The next nonce your journal would hand out. Stop your bot (or pause the policy) before reading it.const nextJournalNonce = BigInt(process.env.NEXT_NONCE!);
const makerEthAddress = quoteSigner.address;
const rpc = createSolanaRpc(process.env.RPC_URL!);
const { maker } = awaitderiveMaker(makerEthAddress);
// Read the current operator and floor: configureMaker replaces BOTH.const { value } = await rpc.getAccountInfo(maker, { commitment: 'finalized', encoding: 'base64' }).send();
if (!value || value.owner !== PROGRAM_ID) throwError('maker account missing');
const current = awaitdecodeMaker(maker, Buffer.from(value.data[0], 'base64'), makerEthAddress);
// Every nonce your journal ever handed out is below nextJournalNonce.// Raising the floor to it cancels all of them.const minNonce = nextJournalNonce > current.minNonce ? nextJournalNonce : current.minNonce;
const { instruction } = awaitconfigureMaker({
authority: authority.address,
makerEthAddress,
operator: current.operator, // keep the operator; the zero key would revoke it
minNonce,
});
console.log('floor raised to', minNonce, awaitsend(authority, instruction));
Set the floor to the next nonce your journal would hand out. That cancels everything you have signed and leaves you free to keep quoting, unless your quote key leaked.
Caution
Do not set the floor to 2^64 − 1 unless you are retiring a leaked key (If your quote key leaks). It retires this quote key forever: no quote can ever fill again, and the key can never be registered again. To stop for now, pause the mint policy instead.
Most KMS products sign a 32-byte digest and return r and s with no recovery byte. This adapter:
hashes the EIP-712 message;
asks your KMS to sign the digest;
folds s into the lower half, which the program requires;
finds the recovery byte by checking which one recovers your quote key.
ts
import { Signature, TypedDataEncoder, recoverAddress, toBeHex, zeroPadValue } from'ethers';
import { assertCanonicalSignature, typeTypedDataSigner } from'@covrfun/maker-sdk';
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; // secp256k1 order/** Wrap any KMS or HSM that signs a 32-byte digest and returns r and s (no v). */exportfunctionkmsSigner(quoteKeyAddress: string,
signDigest: (digest: string) => Promise<{ r: bigint; s: bigint }>,
): TypedDataSigner {
return {
asyncsignTypedData(domain, types, value) {
const digest = TypedDataEncoder.hash(domain, types, value);
let { r, s } = awaitsignDigest(digest);
if (s > N / 2n) s = N - s; // the program accepts low-s onlyconst rHex = zeroPadValue(toBeHex(r), 32), sHex = zeroPadValue(toBeHex(s), 32);
for (const v of [27, 28] asconst) {
const signature = Signature.from({ r: rHex, s: sHex, v }).serialized;
if (recoverAddress(digest, signature).toLowerCase() === quoteKeyAddress.toLowerCase()) {
assertCanonicalSignature(signature);
return signature;
}
}
throwError('KMS signature does not recover to the quote key');
},
};
}
signIntent and signQuote check the result again. They refuse a signature that does not recover to the maker named in the message, or one with a high s.
Each entry carries standard eth_signTypedData_v4 JSON (domain, types, primaryType, message), the expected digest and the expected signature. Feed the typed data to your stack and compare:
Digests must match exactly. A different digest means your encoding differs from covr's.
Signatures match exactly only for deterministic signers (RFC 6979), such as ethers. Many KMS and HSM services pick a random nonce. Their signatures differ from the vectors and are still valid. For those, check that each signature recovers to the vector address and passes assertCanonicalSignature.
ts
import { readFile } from'node:fs/promises';
import { createRequire } from'node:module';
import { Wallet, TypedDataEncoder, keccak256, recoverAddress, toUtf8Bytes } from'ethers';
import { assertCanonicalSignature } from'@covrfun/maker-sdk';
constrequire = createRequire(import.meta.url);
const file = require.resolve('@covrfun/maker-sdk/vectors/golden-vectors.json');
const v = JSON.parse(awaitreadFile(file, 'utf8'));
// The vectors' throwaway key. Replace this signer with your KMS or HSM adapter.const signer = newWallet(keccak256(toUtf8Bytes(v.key.seed)));
for (const name of ['registration', 'bidIntent', 'quote', 'withdrawal']) {
const { domain, types, message } = v[name].typedData;
const { EIP712Domain, ...signedTypes } = types; // ethers derives the domain type itselfvoidEIP712Domain;
const digest = TypedDataEncoder.hash(domain, signedTypes, message);
const signature = await signer.signTypedData(domain, signedTypes, message);
if (digest !== v[name].digest) { console.log(name.padEnd(12), 'DIGEST MISMATCH', digest); continue; }
assertCanonicalSignature(signature); // throws on a high-s signatureconst recovers = recoverAddress(digest, signature) === v.key.address;
// Deterministic signers (RFC 6979, like ethers) reproduce the vector exactly.// A random-nonce KMS gives a different, equally valid signature: it must still recover.console.log(name.padEnd(12), signature === v[name].signature ? 'match' : recovers ? 'valid (recovers)' : 'SIGNATURE MISMATCH');
}
Expected output:
text
registration match
bidIntent match
quote match
withdrawal match
To test your KMS adapter itself, import the throwaway key into a test KMS key and wrap it with kmsSigner. Then run the same loop, checking the digest exactly and the signature by recovery.
Caution
The golden-vector key is public. Never fund it or register it.
Your quote key lives in a KMS or HSM, and your adapter matches every golden vector.
Your authority is in cold or well-guarded storage. It is needed for setup, deposits, withdrawals, configureMaker, re-enabling a paused policy, and exercise (unless an operator exercises).
Optional: an operator key, set with configureMaker, exercises so the authority can stay offline.
Your gateway token is stored as a secret, never in code or logs.
Your exposure, mint policy caps and premium budget match the risk you want. The premium limits cap what forged fills from a leaked quote key could cost; forged early closes stop only when you retire the key.
Your vault holds enough USDC for the premiums you expect, plus the strikes you plan to pay or a tested top-up plan.
Your authority's JitoSOL token account exists, for exercise deliveries (setup step 8).
@covrfun/maker-sdk is the TypeScript toolkit a market-making desk uses to quote covr's covered calls on Solana. It derives and decodes your on-chain accounts, builds every setup and settlement instruction, signs gateway messages with your quote key, does the exact USDC arithmetic the program does, and speaks the gateway's WebSocket protocol.
examples/maker/index.ts and examples/maker/README.md (an example bot), README.md
Dev tooling
None required. To run a .ts file directly, use node file.ts (Node 24 strips types) or install tsx (npm install -D tsx) and run npx tsx file.ts. Node does not strip types inside node_modules, so copy the example bot out before running it.
Runtime dependencies, pinned to exact versions (the source and the built package.json pin the same three):
covr turns gateway access on per desk. Until covr lists your desk on a market, your bid intents are verified exactly like real ones and then refused with Bid intent verified but not entered: this market does not list this maker yet (practice mode). Desks quote JitoSOL ordinary markets; tokenized-stock (restricted) markets stay covr-only. Markets a desk creates are not shown to customers yet.
It builds and signs. It never prices. There is no pricing model and no default price anywhere. Every price is an argument you pass. exactPriceAtOrBelow only rounds your price to one the program can settle exactly.
It never holds keys. Signing functions take a TypedDataSigner: an ethers Wallet or your KMS or HSM adapter. Nothing loads a key from disk or the environment.
It never picks an RPC. No function reads the chain. You read accounts from your own RPC at finalized commitment, check their owner, and pass the bytes to the decoders.
It never submits. Builders return @solana/kit instructions. Your pipeline adds them to a transaction, signs with your Solana authority and sends.
No environment. The SDK reads no environment variable. Mainnet identities are literals in constants.
Exact or refuse. Amounts that the program would refuse make the SDK throw. Nothing is rounded except the protocol fee, which rounds down as the program does.
covr's own code. The decoders, the signers and most builders call the same code covr's own maker and gateway use. The vault, premium budget, deposit, withdraw and exposure-limit encoders, and the fresh writer account recipe of the exit helpers, are the SDK's own and are tested byte for byte against covr's.
Every function that derives, decodes or builds against a program address takes an optional last argument d: Deployment, defaulting to MAINNET. Pass another Deployment only for a local validator or tests.
ts
import { deriveMaker, MAINNET, typeDeployment } from'@covrfun/maker-sdk';
declareconstlocalProgramId: string; // a program you deployed to a local validatorconstlocal: Deployment = { ...MAINNET, programAddress: localProgramId };
const onMainnet = awaitderiveMaker('0x7B99e6A7407Fe0058119F4f13494C0b9cec35738');
const onLocal = awaitderiveMaker('0x7B99e6A7407Fe0058119F4f13494C0b9cec35738', local);
Market decoding always requires the mainnet genesis hash and the mainnet USDC mint, exactly as the program does. A Deployment with another genesisHash cannot decode any market.
Public mainnet identities, signing-domain inputs and gateway timing that the SDK exports. Every value here is a literal or is computed once when the module loads. Nothing reads the environment, an RPC or a key.
Meaning: owner of every Maker, MakerOriginV1, MarketV3, PositionV3, Exposure, PremiumBudget, MintPolicyV1 and MintUsageV1 account the SDK derives. Vaults are different: your USDC vault, each position's collateral vault and its strike escrow are SPL token accounts owned by their token program, with a program-derived address as token authority (your Maker account for the USDC vault).
Where it matters: check that every account you read (Maker, MarketV3, PositionV3) is owned by this address before you decode it. The decoders do not see the account owner.
Warning
The program is upgradeable. Read its upgrade authority from the program's ProgramData account before you fund a maker vault.
ts
import { PROGRAM_ID } from'@covrfun/maker-sdk';
declareconstaccount: { owner: string; data: Uint8Array } | null;
if (!account || account.owner !== PROGRAM_ID) thrownewError('not a covr program account');
Meaning: separates this deployment's signatures from any other deployment of the same code. A grant or quote signed for one deployment never verifies on another.
Meaning: every MarketV3 and PositionV3 account stores it. The decoders require it.
Where it matters:decodeMarketV3 refuses any market whose stored genesis is not this value, even if you pass a Deployment with another genesisHash. The program pins the same value.
The name, version and chainId shared by every covr EIP-712 domain. Each real domain adds its own verifyingContract: one for the registration grant, one per market. The object is frozen.
Where it matters: a mint policy holds one terms id. A policy built from an ordinary market (with mintPolicySettingsFor) carries this value and admits fills only in ordinary markets.
The termsId of restricted markets (settlement mode 1, fund then deliver) with the approved 7-day cancellation timeout: sha256("COVR:RESTRICTED-PENDING:1\0" || 0x01 || u64_le(604800)), as 0x-hex.
The scope string hashed into the registration grant's verifyingContract.
ts
exportdeclareconstREGISTRATION_SCOPE: string;
Value:'registration-v3'
Where it matters:registrationGrantDomain derives verifyingContract = last20(keccak256("deployment-v2" || deploymentId || programId || "registration-v3")). For mainnet that is 0xd2d2b887d02b01d3a4371c7b77f96c3880513b4e.
Computes the 20-byte wire alias of a mint. Use it to match an RFQ's asset to a mint, or to subscribe to a feed by mint.
RFQs, intents and quotes name tokens by this alias, not by the Solana mint. The alias is the last 20 bytes of keccak256 over the mint's 32 public-key bytes, as lowercase 0x-hex. It is an identifier only, never an EVM contract.
Where it matters:asset, collateralAsset and assetAddress of every JitoSOL RFQ, intent and quote. The JitoSOL feed path is /rfqs/0x43127405c3191a0b63a5f3d845f616ffea348c68.
Where it matters:GatewayClient refuses to send a request whose JSON frame is larger, and closes a socket that receives a larger frame. A signed bid intent or quote frame is under 1 KiB (844 and 931 bytes in the golden vectors).
How long the winning maker has to answer an award with a signed quote, in milliseconds.
ts
exportdeclareconstAWARD_MS = 3000;
Value:3000 ms
Meaning: each award's awardUntil = min(award time + 3000, hardDeadline), rounded down to a whole millisecond. A decline, a disconnect or a failed answer passes the award to the next bidder at once.
The longest life a quote may have, in seconds, measured on the gateway's clock when it admits the quote.
ts
exportdeclareconstREVIEW_SECONDS = 60;
Value:60 s
Meaning: the gateway admits a quote only if now < validUntil <= now + 60. defaultIntentValidUntil also uses it: min(option expiry, ceil(hardDeadline / 1000) + 60).
Your quote key is a secp256k1 key with an Ethereum-style 0x… address. It signs four kinds of EIP-712 typed data: the one-time registration grant, sealed bid intents, executable quotes, and quote withdrawals (a route not served to desks). This page covers the signer shape the SDK accepts, the signature rule the program enforces, how covr builds its domains, and how to prove your signer against the golden vectors before you sign anything real.
The signer interface every signing function takes. An ethers Wallet satisfies it as is. For a KMS, HSM or remote signing service, write a small adapter.
One primary type (Register, BidIntent, QuoteV2 or WithdrawBid). It never includes EIP712Domain.
value
Record<string, unknown>
The encoded message, not the wire object. Solana keys arrive as 32-byte bytes32 hex, the RFQ id as keccak256(utf8(id)). See What your signer receives.
Returns:Promise<string>. The 65-byte signature r || s || v as 0x plus 130 hex characters, with v in {27, 28} and s in the low half of the curve order.
Every SDK signing function checks the result with assertCanonicalSignature, then recovers the signer with ethers.verifyTypedData and refuses a signature that does not recover to the expected key. It returns the signature lowercased.
0x plus 130 hex characters: r (32 bytes), s (32 bytes), v (1 byte). Upper- or lowercase hex.
Returns:void.
Throws:
Condition
Message
Not a string, or not exactly 0x + 130 hex characters
maker-sdk: signature must be 65 bytes of 0x-hex
v is not 27 or 28, s is zero, or s > n/2 (high-s)
maker-sdk: non-canonical signature (need v in {27,28} and low s)
n/2 is 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0. The Solana secp256k1 recovery syscall alone would accept the high-s twin of a valid signature. The program refuses it, and so does this check.
ts
import { assertCanonicalSignature } from'@covrfun/maker-sdk';
declareconstsignature: string; // from your own signing pathassertCanonicalSignature(signature); // throws on high-s, v not in {27,28}, or bad length
So a market's domain commits to every term. A signature for one market never verifies for another market. The paused flag and the PDA bump are not part of the scope, so pausing a market does not change its domain.
The package ships vectors/golden-vectors.json, importable as @covrfun/maker-sdk/vectors/golden-vectors.json. It holds fixed inputs and the exact bytes covr's own code produces for them. Use it to prove that your EIP-712 encoder and your signer produce what the gateway and the program verify.
All signatures in the file come from a throwaway key that is public by construction:
Check three things for each typed-data vector (registration, bidIntent, quote, withdrawal):
Digest. Your EIP-712 encoder gives digest for typedData (drop EIP712Domain from types if your library adds it itself). This proves your types, domain and value encoding.
Signature format. Sign typedData with your own key. The result passes assertCanonicalSignature and recovers to your quote key's address.
Signature bytes (deterministic signers only). If your stack can load the throwaway key and signs with RFC 6979 (ethers does), the signature equals signature byte for byte. Many KMS and HSM services use a random nonce. Their signatures differ from the vector and are still valid: rely on checks 1 and 2.
Then check the domains and the encoding the SDK computes for you:
marketDomain(market, bytes of accountDataBase64) equals domain for both market vectors.
This script runs as is under Node 24, which strips TypeScript types itself: node verify-vectors.ts. With the tsx dev tool (npm install -D tsx) you can also run npx tsx verify-vectors.ts. Replace mySigner with your adapter.
ts
import { readFileSync } from'node:fs';
import { createRequire } from'node:module';
import { TypedDataEncoder, Wallet, keccak256, toUtf8Bytes, verifyTypedData } from'ethers';
import {
assertCanonicalSignature,
encodeCancellationRequestV1,
marketDomain,
typeTypedDataSigner,
} from'@covrfun/maker-sdk';
constrequire = createRequire(import.meta.url);
const vectors = JSON.parse(
readFileSync(require.resolve('@covrfun/maker-sdk/vectors/golden-vectors.json'), 'utf8'),
);
// The public throwaway key. Never use it for anything else.const throwaway = newWallet(keccak256(toUtf8Bytes(vectors.key.seed)));
if (throwaway.address !== vectors.key.address) thrownewError('key derivation differs');
// Replace with your KMS/HSM adapter and its address.constmySigner: TypedDataSigner & { address: string } = throwaway;
for (const name of ['registration', 'bidIntent', 'quote', 'withdrawal']) {
const { types: allTypes, domain, message } = vectors[name].typedData;
const { EIP712Domain: _ignored, ...types } = allTypes;
// 1. Digest: proves your types, domain and value encoding.if (TypedDataEncoder.hash(domain, types, message) !== vectors[name].digest)
thrownewError(`${name}: digest differs`);
// 2. Format: canonical, and recovers to your key.const signature = await mySigner.signTypedData(domain, types, message);
assertCanonicalSignature(signature);
if (verifyTypedData(domain, types, message, signature) !== mySigner.address)
thrownewError(`${name}: signature does not recover to your key`);
// 3. Bytes: only for a deterministic (RFC 6979) signer holding the throwaway key.const reference = await throwaway.signTypedData(domain, types, message);
if (reference !== vectors[name].signature) thrownewError(`${name}: signature bytes differ`);
}
// 4. Market domains from raw account bytes.for (const name of ['ordinaryMarketDomain', 'restrictedMarketDomain']) {
const raw = newUint8Array(Buffer.from(vectors[name].accountDataBase64, 'base64'));
const domain = awaitmarketDomain(vectors[name].market, raw);
if (domain.verifyingContract !== vectors[name].verifyingContract)
thrownewError(`${name}: domain differs`);
}
// 5. Cancellation request encoding.const r = vectors.cancellationRequest.request;
const encoded = encodeCancellationRequestV1({
...r,
intentGeneration: BigInt(r.intentGeneration),
refundStrikeUsdc: BigInt(r.refundStrikeUsdc),
returnCollateralAtoms: BigInt(r.returnCollateralAtoms),
validUntil: BigInt(r.validUntil),
});
if ('0x' + Buffer.from(encoded).toString('hex') !== vectors.cancellationRequest.encodedHex)
thrownewError('cancellation encoding differs');
console.log('all golden vectors verified');
On-chain setup and upkeep for a maker: derive your accounts, register your quote key, open and fund your USDC vault, set the caps the program enforces on every fill, and later change them or withdraw. Every function here builds an instruction, computes an address or decodes bytes you read. None signs a transaction, reads the chain or submits anything. The program re-checks every authority and amount.
A fill passes the exposure, premium budget, mint policy, mint usage and origin accounts to the program, so steps 1 to 5 must exist before any fill can land. Step 7 funds the premium the vault pays at fill and the strike it pays at exercise.
Builders return a @solana/kit instruction. Its accounts carry roles (0 read-only, 1 writable, 2 read-only signer, 3 writable signer) but no signer objects. Add it to a transaction with your own pipeline and sign with your authority.
ts
import {
appendTransactionMessageInstructions,
createSolanaRpc,
createTransactionMessage,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
typeTransactionSigner,
} from'@solana/kit';
import { initializeVault, deriveMaker } from'@covrfun/maker-sdk';
declareconstauthority: TransactionSigner; // your Solana authoritydeclareconstquoteKeyAddress: string; // your 0x quote key addressconst rpc = createSolanaRpc('https://<your rpc>');
const { maker } = awaitderiveMaker(quoteKeyAddress);
const { instruction } = awaitinitializeVault({ authority: authority.address, maker });
const { value: blockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
const message = pipe(
createTransactionMessage({ version: 0 }),
m =>setTransactionMessageFeePayerSigner(authority, m),
m =>setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
m =>appendTransactionMessageInstructions([instruction], m),
);
const signed = awaitsignTransactionMessageWithSigners(message);
// Simulate, send and confirm `signed` with your own RPC at finalized commitment.
Builders that return recommendedComputeUnitLimit expect you to prepend a compute-unit-limit instruction with at least that value, and to simulate first.
Two changes have no builder. The SDK cannot pause a market you created. Nothing can change the exposure cap of a Token-2022 collateral: the program's set_exposure_limit accepts only a classic SPL Token mint.
Returns:vault, the PDA ["stable", maker, USDC mint], a USDC token account whose token authority is your Maker account; premiumBudget, the PDA ["premium-budget", maker, USDC mint].
Throws:SolanaError from @solana/kit when maker or d.programAddress is not a valid address.
Derives the PositionV3 a fill of your quote with a given nonce creates, plus its collateral vault, strike escrow, policy and usage. Use it to watch the chain for your fills: the gateway sends no fill message.
Returns: a frozen object with maker, position (PDA ["position", 20 address bytes, nonce u64 LE]), collateralVault (["collateral", position]), strikeEscrow (["strike-escrow-v3", position]), policy, usage, and a ...Bump for each.
The position address depends only on your quote key and the nonce, not on the market. One nonce can open at most one position across all your markets, so never reuse a nonce.
Throws:
Condition
Message
Bad makerEthAddress
PositionV3: invalid hex identity or PositionV3: zero hex identity
collateral or d.programAddress is the zero key
PositionV3: zero identity
nonce is not a u64 bigint
PositionV3: invalid nonce
nonce is 2^64 - 1 (reserved)
PositionV3: reserved nonce
ts
import { derivePosition, JITOSOL_MINT } from'@covrfun/maker-sdk';
declareconstquoteKeyAddress: string;
const { position } = awaitderivePosition(quoteKeyAddress, 1001n, JITOSOL_MINT);
// A PositionV3 at `position` owned by the program means quote nonce 1001 filled.
Returns:signature (lowercase 0x-hex, 65 bytes); signer, the checksummed address the signature recovers to; and the exact domain, types and message that were signed.
The function does not compare signer with any expected key. Check it equals your quote key address before you register.
Builds register_maker_v3, which creates your Maker and MakerOriginV1 accounts. It verifies the grant locally first, so a wrong domain, key or authority fails before you pay any fee.
Returns:maker, origin, and the instruction. Accounts: authority (writable signer), maker (writable), origin (writable), System program. Data: the 8-byte discriminator, the 20 address bytes, the 65 signature bytes.
On chain the program verifies the grant again and creates a 117-byte Maker with no operator (the zero key) and min_nonce = 0. The authority and the quote key can never change afterwards.
Builds initialize_vault, which creates your USDC vault and your PremiumBudget. The budget starts disabled: no lifetime premium cap applies until you call setPremiumBudget.
The cap compares against everything spent since the budget was created. Changing it never resets spent. While enabled, a fill fails when spent + gross premium > limit. A limit below what you have already spent blocks every fill.
Returns:vault and an SdkInstruction. Accounts: authority (read-only signer), maker, USDC mint, vault (writable), destination (writable), Token program. These are the program's deposit accounts, with the destination in the slot a deposit uses for its source. The program pays out under your Maker account's authority.
Warning
Nothing in the vault is reserved for your open positions. Each exercise pays the strike from the vault, and each fill pays the gross premium from it. Withdraw too much and those exercises and fills fail until you deposit again.
Throws:
Condition
Message
amount is not a bigint in [0, 2^64 - 1]
maker-sdk: amount must be a u64 bigint
amount is 0n
maker-sdk: withdraw amount must be positive
destination equals the vault, the maker or the authority
maker-sdk: destination must be the authority's own USDC token account
Bad authority, maker or destination
maker-sdk: <name> address required, maker-sdk: <name> must not be the zero key, or SolanaError
The program also requires amount > 0, the destination's mint to be USDC and its owner to be the authority.
Builds initialize_exposure_v3, which creates the Exposure account for one collateral mint: a cap on the collateral atoms held across all your open positions on that mint.
Returns:maker, exposure and an SdkInstruction. Accounts: authority (read-only signer), maker, collateral mint, exposure (writable). Data: discriminator, limitAtoms (u64 LE).
The cap gates new fills only. Positions already open above a lowered cap stay open.
Note
Classic SPL Token collateral only. The program reads the collateral account as a classic SPL Token mint, so it cannot change a Token-2022 collateral's cap. The SDK refuses that case instead of letting it fail on chain.
Throws:
Condition
Message
limitAtoms is not a bigint in [0, 2^64 - 1]
maker-sdk: limitAtoms must be a u64 bigint
Bad authority
maker-sdk: authority address required, maker-sdk: authority must not be the zero key, or SolanaError
collateral.mint missing or the zero key
maker-sdk: collateral mint address required / maker-sdk: collateral mint must not be the zero key
collateral.program is not the SPL Token program
maker-sdk: set_exposure_limit reads the collateral as a legacy SPL Token mint: a Token-2022 collateral cap cannot be changed
Builds mint policy settings bound to one market's termsId and profileHash, with your caps. Pass a decoded market. The result goes to initializeMintPolicyV1.
Optional. A key that may pause the policy (and do nothing else). Default: the zero key 11111111111111111111111111111111, meaning no watcher.
caps.enabled
boolean
Optional. Default true. A disabled policy refuses every fill on the mint.
caps.maxOrderAtoms
bigint
Largest single fill, in collateral atoms.
caps.maxReservedAtoms
bigint
Most collateral atoms open at once on this mint.
caps.maxReservedStrikeUsdc
bigint
Most strike obligation open at once on this mint, in USDC atoms.
caps.lifetimePremiumLimitUsdc
bigint
Optional. Lifetime gross premium cap on this mint, in USDC atoms. Omitted means no cap.
Returns:MintPolicySettingsV1. lifetimePremiumLimitEnabled is true exactly when you passed lifetimePremiumLimitUsdc. termsId and profileHash are copies.
A policy holds one termsId, so one mint is quoted in ordinary markets or in restricted markets, never both. The policy also admits only markets with the same profileHash (the mint's committed token profile).
Warning
lifetimePremiumLimitUsdc: 0n is a cap of zero, not "no cap": the first fill's premium already exceeds it, so the mint takes no fills. Omit the field for no mint-level lifetime cap.
import { mintPolicySettingsFor, typeDecodedMarketV3 } from'@covrfun/maker-sdk';
declareconstmarket: DecodedMarketV3; // from decodeMarketV3const settings = mintPolicySettingsFor(market, {
maxOrderAtoms: 1_000_000_000n, // 1 JitoSOL per fill (the program caps a fill at 2 SOL of value)maxReservedAtoms: 100_000_000_000n, // 100 JitoSOL openmaxReservedStrikeUsdc: 25_000_000_000n, // 25,000 USDC of strike open
});
Builds update_mint_policy_v1, which replaces every setting of one mint policy. It applies only if the policy is still at expectedGeneration; the generation then goes up by one.
Returns:policy, usage and the instruction. Accounts: authority (read-only signer), maker, collateral mint, USDC mint, policy (writable), usage. Data: discriminator, expectedGeneration (u64 LE), then the settings as in initializeMintPolicyV1.
If another update or a pause landed first, the generation has moved and the program refuses yours. Read the policy again and rebuild.
Throws:
Condition
Message
Bad authority or maker
maker-sdk: authority address required, maker-sdk: authority must not be the zero key, maker-sdk: maker address required, maker-sdk: maker must not be the zero key, or SolanaError
expectedGeneration is not a bigint with 0 < g < 2^64 - 1
Builds pause_mint_policy_v1, which sets the policy's enabled to false so the mint takes no new fills. Your authority or the policy's watcher may sign it.
Returns:policy and the instruction. Accounts: signer (read-only signer), maker, policy (writable). Data: discriminator, expectedGeneration (u64 LE).
Like an update, it applies only at expectedGeneration and moves the generation up by one. It works even if the collateral mint has since been frozen, closed or changed. To resume, send updateMintPolicyV1 with enabled: true.
Throws:
Condition
Message
Bad signer or maker
maker-sdk: signer address required, maker-sdk: signer must not be the zero key, the same for maker, or SolanaError
expectedGeneration is not a bigint with 0 < g < 2^64 - 1
Decodes your MintPolicyV1 account and binds it to your maker, authority and collateral. Use it to read the current generation and settings before an update or a pause.
The account you read: deriveMintPolicy(maker, collateral.mint).policy.
raw
Uint8Array
Its 342 bytes, read at finalized commitment. For a pause or update you may read generation at confirmed: a stale value only fails with MintPolicyGeneration (6012).
Builds configure_maker, which sets your operator key and your nonce floor min_nonce. Raising min_nonce is how you cancel signed quotes: every fill of a quote with a lower nonce fails.
Returns:maker and the instruction. Accounts: authority (read-only signer), maker (writable). Data: discriminator, operator (32 bytes), minNonce (u64 LE).
The instruction replaces both fields. There is no default operator: read your current one with decodeMaker and pass it back if you want to keep it. The program refuses a minNonce lower than the current one.
Warning
Desks have no gateway route to withdraw a quote. For an immediate, reversible stop, pause your mint policy first (Cancel quotes). To cancel signed quotes for good, raise min_nonce above their nonces with this instruction. A quote also expires on its own: its validUntil is at most 60 s after the gateway admits it.
Throws:AssertionError (from node:assert) on bad input:
Condition
Message
minNonce is not a bigint in [0, 2^64 - 1]
uint64 bigint required
makerEthAddress is all zeros
Zero maker identity
makerEthAddress is not 0x + 40 hex
Node's generated message, starting The input did not match the regular expression
authority or d.programAddress is the zero key
Node's generated message, starting Expected "actual" to be strictly unequal to
An address is not valid base58
SolanaError from @solana/kit
ts
import { configureMaker, typeDecodedMakerAccount } from'@covrfun/maker-sdk';
declareconstauthority: string;
declareconstquoteKeyAddress: string;
declareconstcurrent: DecodedMakerAccount; // from decodeMaker, read just now// Cancel every signed quote with a nonce below 5000, keep the operator.const { instruction } = awaitconfigureMaker({
authority,
makerEthAddress: quoteKeyAddress,
operator: current.operator,
minNonce: 5000n,
});
A MarketV3 account holds one option series: collateral mint, strike, expiry, exercise window, size cap and fee. Every RFQ names its market. Read that account from your own RPC, decode it with decodeMarketV3, and sign intents and quotes in that market's own EIP-712 domain (quoteDomain). The SDK also builds the instructions that create markets.
There are two settlement modes:
settlementMode
Name
Exercise
Who quotes it today
0
ordinary
atomic: one exercise_v3 pays strike and delivers collateral
desks (JitoSOL) and covr
1
restricted
two steps: fund the strike, then deliver; carries a 7-day cancellation timeout
The function checks the discriminator, the layout, the mainnet genesis hash and USDC mint, d.deploymentId, every term bound (window 60 to 3,600 s, strike and size above zero, fee at most 1,000 bps, a supported token program, no native SOL), the terms id of the mode, and that marketAddress is the canonical PDA of the creator and salt stored in the bytes.
Warning
It never sees the account owner. Confirm the owner is PROGRAM_ID yourself. It also does not check paused or expiry against your clock: skip a market that is paused or expired.
Returns:TypedDataDomain (ethers): name: 'SOL Covered Calls', version: '1', chainId: 2000000001, and a verifyingContract derived from every immutable term of this market. See How covr domains are built. Ordinary and restricted markets use different scope literals.
Throws: only for a market object you built by hand: MarketV3 hash, MarketV3 integer or MarketV3 fee (ordinary); RestrictedV3: nonzero hash required, RestrictedV3: integer out of range or RestrictedV3: MarketV3 fee (restricted); SolanaError for a bad address.
ts
import { quoteDomain, typeDecodedMarketV3 } from'@covrfun/maker-sdk';
declareconstmarket: DecodedMarketV3;
const domain = quoteDomain(market); // pass to signIntent and signQuote
Returns:market (the PDA ["market-v3", creator, salt]) and the instruction. Accounts: creator (writable signer), market (writable), collateral mint, USDC mint, System program. Data: discriminator, salt (u64), feeOwner (32 bytes), expiry (i64), window (u64), strikeE8 (u64), maxQuantityAtoms (u64), feeBps (u16), all little-endian.
The program then checks the chain clock (60 <= expiry - now <= 7,776,000 s, which is 90 days) and requires a plain collateral mint: an 82-byte mint account (no Token-2022 extensions), not native SOL, with no freeze authority. It records the mint's token profile as profileHash.
Note
A market you create is valid on chain, but customers do not see it yet.
Throws:
Condition
Message
nowUnix missing or not a bigint in [0, 2^63 - 1]
maker-sdk: nowUnix (your clock, unix seconds) is required
salt is not a u64 bigint
maker-sdk: salt must be a u64 bigint
expiry - nowUnix is below 60 or above 7,776,000
maker-sdk: expiry must be 60 s to 90 days after nowUnix
window is not 60 to 3,600
maker-sdk: window must be 60..3600 seconds
strikeE8 is not a positive u64
maker-sdk: strikeE8 must be a positive u64
maxQuantityAtoms is not a positive u64
maker-sdk: maxQuantityAtoms must be a positive u64
feeBps is not an integer 0 to 1,000
maker-sdk: feeBps must be an integer 0..1000
feeOwner is the zero key
maker-sdk: fee owner must not be the zero key
collateral is native SOL (classic or Token-2022)
maker-sdk: native SOL mints are not supported collateral
Builds create_restricted_market_v3, which creates a restricted market for freezable or tokenized-stock collateral with the approved 7-day cancellation timeout.
Returns:market, termsId (32 bytes, equal to RESTRICTED_TERMS_ID), recommendedComputeUnitLimit (200,000) and the instruction. Accounts: creator (writable signer), market (writable), collateral mint, USDC mint, System program. Data: as create_market_v3, then the timeout (u64 LE).
Builders and signers for the two messages a desk sends in an opening auction: a sealed bid intent while the RFQ collects, and an executable quote when you win the award. The builders only copy fields. The price is always yours: the SDK has no pricing logic.
The customer's wallet signs and submits the fill. You see it only on chain, as your position PDA (derivePosition).
Note
Until covr lists your desk on a market, every bid intent is verified exactly like a real one and then refused with Bid intent verified but not entered: this market does not list this maker yet ("practice mode"). It never wins and never moves a price.
Gross premium per one whole collateral token, in USDC × 10^18 (priceE18), as a decimal string on the wire. 2400000000000000000 is 2.40 USDC per token.
quantity
Collateral atoms scaled to 18 decimals (quantityE18), decimal string. For JitoSOL (9 decimals), 1 atom is 1000000000. Convert with atomsFromWire.
strike
Strike in USD × 10^8 (strikeE8), decimal string. 17000000000 is $170.
expiry, validUntil
Unix seconds, integer number.
nonce
u64 as a decimal string.
closesAt, hardDeadline, awardUntil
Epoch milliseconds.
The gross premium of a fill is atoms × price / 10^(decimals + 12) USDC atoms, and it must be exact. The gateway refuses an intent whose premium is not a whole USDC atom. Round your price with exactPriceAtOrBelow.
Builds the unsigned sealed bid intent for an opening RFQ. It copies every instrument field verbatim from the snapshot, because the gateway compares them as exact strings.
Builds the unsigned executable quote that answers an award. It takes the maker, price and instrument from your intent, and binds the quote to this auction and to the writer's wallet.
Beyond what this function checks, the quote must also pass:
the gateway: now < validUntil <= now + 60 on its clock, and the award not yet expired;
the program at fill: chain clock < validUntil, validUntil <= expiry, nonce >= min_nonce, and the position PDA for this nonce not existing yet.
Leave a few seconds of margin: covr's example bot uses min(expiry, now + 55).
Warning
Write the nonce, terms and the cash it can cost to durable storage before you sign. A reply that times out can still mean a live quote, and only the chain settles it.
Throws:
Condition
Message
intent.auctionId !== snapshot.id
maker-sdk: intent belongs to another auction
nonce is not a bigint in [0, 2^64 - 2]
maker-sdk: nonce must be a u64 below 2^64-1 (that value is reserved)
validUntil not a positive safe integer, or after intent.expiry
maker-sdk: validUntil must be unix seconds no later than the option expiry
If the award's intent is not the one you stored, decline it at once. Throwing or staying silent holds the award until the hard deadline, and the customer gets no quote.
ts
import {
buildQuote,
REVIEW_SECONDS,
typeAward,
typeBidIntent,
typeGatewayClient,
typeRfqSnapshot,
} from'@covrfun/maker-sdk';
declareconstclient: GatewayClient;
declareconstsnapshot: RfqSnapshot;
declareconstmyIntent: BidIntent; // the intent you signed and stored for award.iddeclarefunctionreserveNonceDurably(): Promise<bigint>; // your journal: fsync before signingasyncfunctiononAward(award: Award) {
if (JSON.stringify(myIntent) !== JSON.stringify(award.intent)) {
await client.sendAwardDecline(award.id, award.intent.signature); // pass it on nowreturn;
}
const nonce = awaitreserveNonceDurably();
const now = Math.floor(Date.now() / 1000);
const unsignedQuote = buildQuote(snapshot, myIntent, {
nonce,
validUntil: Math.min(snapshot.request.expiry, now + REVIEW_SECONDS - 5),
});
// Sign with signQuote, then client.sendQuote(myIntent.signature, quote).
}
maker-sdk: signature recovers to <recovered>, not the maker <maker>
ts
import { quoteDomain, signIntent, typeDecodedMarketV3, typeTypedDataSigner, typeUnsignedBidIntent } from'@covrfun/maker-sdk';
declareconstsigner: TypedDataSigner;
declareconstmarket: DecodedMarketV3;
declareconstunsigned: UnsignedBidIntent; // from buildBidIntentconst intent = awaitsignIntent(signer, quoteDomain(market), unsigned);
// Store `intent` by RFQ id before sending it: you answer the award from this copy.
Desks cannot use this today. The gateway serves the withdrawal route only to covr's own maker, not on the desk listener. A withdrawal would never cancel a quote on chain anyway. For an immediate, reversible stop, pause your mint policy (Cancel quotes). To cancel signed quotes for good, raise min_nonce with configureMaker.
Exact USDC arithmetic for a fill, the position decoder, the builders for fill, exercise and release, and one-transaction helpers that assemble a whole exercise or release. The math returns exactly what the program computes and throws wherever the program would refuse the amount. Nothing here signs or submits.
atoms × priceE18 / 10^(decimals + 12), must divide exactly, must be > 0 and ≤ 2^64 - 1
strike payment
USDC atoms
atoms × strikeE8 / 10^(decimals + 2), must divide exactly, must be > 0 and ≤ 2^64 - 1
fee
USDC atoms
floor(gross × feeBps / 10000), feeBps 0 to 1,000
writer premium
USDC atoms
gross - fee
wire quantity
collateral atoms × 10^(18 - decimals)
must convert exactly both ways
Nothing is rounded except the fee, which rounds down. An amount that does not divide exactly is refused, never rounded. The vault pays the gross premium at fill (the fee is carved out of it) and the strike payment at exercise.
Worked example (the golden-vector quote): 0.5 JitoSOL (500_000_000 atoms, 9 decimals) at priceE18 = 2_400_000_000_000_000_000 (2.40 USDC per JitoSOL), strike $170 (17_000_000_000), fee 1,000 bps:
Returns the one priceE18 that pays exactly gross USDC atoms for atoms. Use it to audit a price, not to choose one: for most sizes no price pays an arbitrary gross exactly.
Rounds a model price down to the greatest priceE18 whose gross premium for exactly atoms is a whole USDC atom. Bid this value, not a price derived from a rounded premium. It is how covr's own maker rounds.
Your model price, USDC × 10^18 per whole token. u128.
atoms
bigint
The RFQ size in collateral atoms. Positive u64.
collateralDecimals
number
Collateral mint decimals, 0 to 255.
Returns:bigint: floor(priceE18 / step) × step, where step = 10^(decimals + 12) / gcd(atoms, 10^(decimals + 12)). 0n means no positive exact price is at or below priceE18: skip the RFQ.
For JitoSOL, 0.5 token (500_000_000 atoms) gives step = 2_000_000_000_000: prices move in steps of 0.000002 USDC per token. A 1-atom RFQ gives step = 10^21, so its smallest exact price is 1,000 USDC per token.
Throws:
Condition
Message
priceE18 out of u128 range
maker-sdk: priceE18 must be a bigint in [0, u128] range
atoms out of u64 range
maker-sdk: atoms must be a bigint in [0, u64] range
Returns the smallest atom count whose strike payment is a whole USDC atom: 10^(decimals + 2) / gcd(strikeE8, 10^(decimals + 2)). Every RFQ quantity is a multiple of it, because the gateway refuses an RFQ whose strike payment is not exact.
This is the smallest exact size, not a size limit. The largest JitoSOL fill is capped by the program at 2 SOL of stake-pool value (JitoSOL fill cap). The SDK does not check that cap.
Throws:
Condition
Message
strikeE8 out of u64 range
maker-sdk: strikeE8 must be a bigint in [0, u64] range
Amounts that compute here can still be refused on chain. For JitoSOL, the program caps one fill at 2 SOL of stake-pool value (about 1.53 JitoSOL at the pool rate of 1.306 SOL per JitoSOL on 15 October 2026; the cap in JitoSOL shrinks as the rate rises); see the JitoSOL fill cap. This function does not check it.
By calling it you assert that both accounts were read at finalized commitment and are owned by the program. Check the owners yourself: the decoder does not see them. It checks the layout, every stored identity (maker, authority, quote key, genesis, deployment, collateral, USDC), the obligations, the clock terms, the funding state, and every PDA and bump.
Throws:PositionV3: <reason>, or a Maker decoding error. The reasons:
Reason
Cause
zero identity, invalid hex identity, zero hex identity
Stored genesis, deployment, collateral or USDC differs
writer drift
input.writer given and the stored writer differs
nonce, admission generation, zero obligation, clock terms
Stored obligations are invalid or the nonce differs
duration outside i64, time overflow, timeout duration outside i64
Time fields overflow
ordinary mode has no cancellation timeout, ordinary mode cannot carry funded or pending rights, funding in unfunded state, funding provenance, funding time, cancellation time
Inconsistent funding state
position PDA, maker PDA, collateral vault PDA, strike escrow PDA, policy or usage PDA
Builds the fill instruction for a signed quote: fill_v3 for an ordinary market, fill_restricted_v3 for a restricted one. The writer (the customer) signs and submits fills; covr's gateway prepares them. Use this to simulate or audit the exact instruction your quote produces.
Accounts: writer (writable signer), market, maker, position (w), collateral vault (w), collateral mint, USDC mint, writer collateral (w), writer USDC (w), fee account (w), maker vault (w), collateral token program, Token program, System program, exposure (w), premium budget (w), mint policy, mint usage (w), origin; then the Jito stake pool (read-only) when the collateral is JitoSOL. Data: discriminator, keccak256(auctionId) (32), nonce (u64), quantity atoms (u64), price (u128), validUntil (i64), signature (65); restricted adds the timeout (u64).
For a restricted market the result also carries termsId and cancellationTimeoutSeconds at run time. The declared type does not list them.
The builder verifies the quote exactly as the program will: market identity and terms, the quote's instrument against the market, quantity equal to wireQuantity(quantityAtoms), the signature against the market domain, exact amounts, and no aliasing between transfer accounts. It does not check time, balances, caps or min_nonce: simulate for those.
Warning
JitoSOL fill cap. The program refuses a JitoSOL fill worth more than 2 SOL of stake-pool value: it requires quantityAtoms × totalLamports <= 2_000_000_000 × poolTokenSupply, read from the Jito stake pool account passed with the fill. A refused fill fails with JitoSolOrderLimit (code 6008). At the pool rate of 1.306 SOL per JitoSOL on 15 October 2026, the cap was about 1.53 JitoSOL. It shrinks as the rate rises. The pool account must also be current for the epoch, or the fill fails with InvalidJitoPool (6009). The SDK does not check either rule: simulate the fill.
Throws:OrdinaryV3: <reason> (restricted: RestrictedV3: <reason>), plus parsing errors from the quote:
Reason
Cause
nonzero hash required, zero identity, unsupported token identity, invalid decimals, ordinary atomic market required (restricted: restricted market required), payment or genesis identity, ordinary terms identity (restricted: restricted terms identity), integer out of range, market terms, market PDA
The market object is not a valid decoded market
fill timeout differs from market terms
Restricted only: timeout mismatch
QuoteV2 required
The quote lacks auctionId or writer
opening paused or quantity exceeds market
Market paused, or quantityAtoms is 0 or above maxQuantityAtoms
nonce, price or e18 quantity mismatch
Nonce is 2^64 - 1, price is 0, or quote.quantity differs from the atoms
quote identity or side mismatch
Writer, asset alias, USDC alias, chain id or side differs from the market
quote time or strike mismatch
Strike or expiry differs from the market, or validUntil is not in (0, expiry]
zero auction, signature length, signature recovery byte
Malformed auction id or signature
fractional USDC obligation, USDC obligation overflow or zero
Builds exercise_v3 for an ordinary position: your vault pays the strike to the writer and the collateral moves to you, in one instruction.
Tip
Most desks should call exerciseV3Instructions instead. It builds the whole transaction: the compute-unit limit, fresh writer accounts and this instruction. Use exerciseV3 only to assemble your own transaction.
The program accepts it only while expiry <= now < expiry + window. Destination token accounts must be initialized, not frozen, owned by the expected owner, with no delegate and no close authority other than the owner. For the writer's accounts, create fresh token accounts owned by the writer in the same transaction, so the writer cannot block your exercise: exerciseV3Instructions does exactly this. See Fills and exercise.
Throws:
Condition
Message
The market is restricted
maker-sdk: restricted markets exercise in two steps: fundRestrictedExerciseV3, then completePendingDeliveryV3
authority is neither the position's maker authority nor its operator
OrdinaryV3: authority differs from observed maker owner/operator
Position not open and ordinary
OrdinaryV3: ordinary open position required
Position obligations or terms differ from the market
OrdinaryV3: invalid claim obligations, OrdinaryV3: ordinary position cannot carry pending rights, OrdinaryV3: position market or deployment mismatch, OrdinaryV3: position token identity mismatch, OrdinaryV3: position immutable terms mismatch
Position object not from the decoder
OrdinaryV3: decoded position observation required
Position addresses not canonical
OrdinaryV3: position canonical PDA mismatch
Two transfer accounts are the same
OrdinaryV3: transfer account alias
A key is the zero key, or the market object is invalid
OrdinaryV3: zero identity and the market reasons of fillV3
Builds release_v3 (ordinary) or release_restricted_v3 (restricted): after the exercise window, anyone may return the collateral to the writer. This frees your exposure and mint usage.
Tip
releaseV3Instructions builds the whole release transaction, with a compute-unit limit of 400,000 and a fresh writer collateral account. Use releaseV3 only to assemble your own transaction.
Returns:addresses, recommendedComputeUnitLimit (400,000 ordinary, 200,000 restricted) and the instruction. Accounts: market, maker, position (w), collateral vault (w), writer, collateral mint, writer collateral (w), collateral token program, exposure (w), mint usage (w). The instruction has no signer account: whoever pays the transaction fee may send it.
The program accepts it only when now >= expiry + window.
Throws: the position and market reasons of exerciseV3 (OrdinaryV3: prefix), or for restricted markets RestrictedV3: restricted position required, RestrictedV3: position state does not permit this action and the same family under RestrictedV3:.
From decodePositionV3, status open. Read once, before any await.
input.authority
string
The signer and fee payer: your maker authority, or your operator. It also pays the rent of both new accounts.
input.makerCollateral
string
Optional. The token account that receives the collateral. Default: your maker authority's associated token account for the collateral mint, under the market's collateral token program.
input.rentLamports
bigint
Rent for one 165-byte token account, in lamports. Any positive u64.
input.seeds
{ writerPayment?, writerCollateral? }
Optional. 32 lowercase hex characters each. Default: 16 random bytes as hex, new on every call.
Returns:
Field
Meaning
instructions
The 8 instructions below, in order.
computeUnitLimit
400000.
rentLamports
2 × input.rentLamports: what authority pays for the two accounts.
writerPayment, writerCollateral
The two new accounts (FreshWriterAccount): address, base, seed, mint, token program.
The default 200,000 is too low: exercise_v3 measured up to 277,316 CU.
1
System Transfer
authority sends rentLamports to the new writer USDC account address.
2
System AllocateWithSeed
Allocates 165 bytes at that address (base authority, the seed), owned by the USDC token program.
3
Token InitializeAccount3
Makes it a USDC token account owned by the writer.
4 to 6
the same three
A new writer collateral account: 165 bytes, collateral mint, collateral token program, owned by the writer.
7
exercise_v3
Pays the strike into account 1 to 3 and the collateral into makerCollateral, and returns any surplus collateral to account 4 to 6.
Why fresh accounts: the program pays the writer only into clean token accounts (no delegate, not frozen, no foreign close authority). An existing writer account could be broken by the writer at any time. These addresses come from createWithSeed(authority, seed, tokenProgram), so only your signer can allocate them. Transfer plus AllocateWithSeed, unlike CreateAccountWithSeed, still work if someone sent lamports to the address first. The writer owns both accounts and keeps their rent. Ordinary markets take only collateral mints without extensions, so both accounts are WRITER_ACCOUNT_SPACE (165) bytes, Token-2022 or not.
Signing and size. Sign with authority as fee payer: one signature. You may add one SetComputeUnitPrice instruction; never a second SetComputeUnitLimit, which the runtime refuses. With a priority fee the transaction is 1,111 of the 1,232 bytes a packet allows, in the worst case (Token-2022 collateral). A separate fee payer adds a signature: 1,207 bytes, which leaves no room for a Jito tip.
Warning
makerCollateral must be your authority's account. The program pays the collateral only into a token account for the collateral mint owned by your maker authority, even when the operator signs. It must not be frozen, have a delegate, or have a close authority other than its owner. Any other account fails every attempt. The default (the authority's associated token account) must already exist: create it before the exercise window opens. The helper cannot see the chain, so it does not check that.
Note
Rent. Read getMinimumBalanceForRentExemption(165) from your RPC at finalized commitment before each exercise, and pass that value. The SDK hard-codes no rate and checks only that rent is positive, because mainnet rent has changed before. Too little rent fails the whole transaction, which simulation shows. Accounts created on mainnet in October 2026 held (128 + size) × 5,080 lamports (1,488,440 for 165 bytes), but read the value; do not compute it.
Throws:
Condition
Message
The market is restricted (checked first)
maker-sdk: restricted markets exercise in two steps: fundRestrictedExerciseV3, then completePendingDeliveryV3
rentLamports is not a bigint in [0, 2^64 - 1]
maker-sdk: rentLamports must be a bigint in [0, u64] range
rentLamports is 0n
maker-sdk: rentLamports must be positive
A seed is not 32 lowercase hex characters
maker-sdk: seed must be 32 lowercase hex characters
The market's token program is neither token program
maker-sdk: token program must be SPL Token or Token-2022
Builds every instruction of one release transaction, ordinary or restricted, in order: after the exercise window, return the collateral to the writer through a fresh account. This is the recommended way to release.
From decodePositionV3, status open. Read once, before any await.
input.payer
string
Any wallet. Signs alone, pays the fee and the rent.
input.rentLamports
bigint
Rent for the new account at its size, in lamports. Any positive u64.
input.collateralAccountSpace
number
Size of the new collateral account in bytes. Required for Token-2022 collateral: the token account size with every account extension the mint requires (175 for covr's tokenized-stock mints, which need TransferHook and Pausable; 165 for a Token-2022 mint without such extensions). For SPL Token collateral omit it, or pass exactly 165.
Returns:instructions (5, below), computeUnitLimit (400000), rentLamports (the input), writerCollateral (FreshWriterAccount) and addresses (as releaseV3 returns).
#
Instruction
What it does
0
Compute Budget SetComputeUnitLimit(400000)
400,000 in both modes: both release instructions take the same accounts, measured up to 233,135 CU.
1 to 3
System Transfer, System AllocateWithSeed, Token InitializeAccount3
A new collateral token account at createWithSeed(payer, seed, collateral token program), of collateralAccountSpace (or 165) bytes, owned by the writer.
4
release_v3 or release_restricted_v3
Returns the collateral into it.
With a priority fee the transaction is 769 of 1,232 bytes, in both modes. The program accepts the release only when now >= expiry + window. Read getMinimumBalanceForRentExemption(size) at finalized commitment for the exact size you pass, as for exerciseV3Instructions. At the October 2026 mainnet rate a 175-byte account needed (128 + 175) × 5,080 = 1,539,240 lamports; read it, do not compute it.
Throws:
Condition
Message
SPL Token collateral with collateralAccountSpace other than 165
maker-sdk: an SPL Token account is exactly 165 bytes
Token-2022 collateral without collateralAccountSpace
maker-sdk: Token-2022 collateral needs collateralAccountSpace: the token account size with every account extension its mint requires (175 for TransferHook + Pausable)
collateralAccountSpace not an integer from 165 to 65,536
maker-sdk: collateralAccountSpace must be an integer from 165 to 65536
Bad rentLamports
maker-sdk: rentLamports must be a bigint in [0, u64] range / maker-sdk: rentLamports must be positive
Bad seed
maker-sdk: seed must be 32 lowercase hex characters
The size in bytes of a token account with no extensions. Every SPL Token account, every writer USDC account and every exercise collateral account the helpers create has this size.
ts
exportdeclareconstWRITER_ACCOUNT_SPACE = 165;
Value:165
Where it matters: the size to price rent for with getMinimumBalanceForRentExemption before exerciseV3Instructions, and before releaseV3Instructions on SPL Token collateral. A Token-2022 collateral account on release is larger when its mint requires account extensions: pass collateralAccountSpace.
Returns:addresses, strikeUsdc (USDC atoms moved into escrow), recommendedComputeUnitLimit (200,000) and the instruction. Accounts: authority (writable signer), market, maker, position (w), collateral vault, collateral mint, collateral token program, USDC mint, maker vault (w), strike escrow (w), mint usage (w), Token program, System program.
Throws:RestrictedV3: authority differs from observed maker owner/operator, RestrictedV3: restricted position required, RestrictedV3: position state does not permit this action, RestrictedV3: account alias, and the restricted market and position reasons (RestrictedV3: prefix).
Builds complete_pending_delivery_v3, step 2 of a restricted exercise: any signer delivers the escrowed strike to the writer and the collateral to the maker.
Throws:RestrictedV3: recipient alias, RestrictedV3: account alias, RestrictedV3: position state does not permit this action, and the restricted market and position reasons.
Encodes a restricted cancellation request to its 288-byte Borsh form, the argument of cancel_pending_delivery_v3. Both parties (the maker authority and the writer) sign that transaction. This SDK has no builder for the instruction itself.
Meaning: tested envelopes, not worst-case guarantees or fee estimates. Prepend your own compute-unit-limit instruction with this value and simulate the full transaction. The chain allows at most 1,400,000 per transaction.
Meaning: provisional envelopes, not measured worst cases. Simulate first. pause (the restricted market pause), cancel, close and bundle have no builder in this SDK. releaseV3Instructions sets 400,000 for a restricted release, not this table's 200,000: both release instructions take the same accounts, measured up to 233,135 CU.
A minimal client for covr's RFQ gateway. One client opens one /maker command socket, one read-only /rfqs/<asset alias> feed socket per asset, and reads RFQ snapshots over GET /api/rfqs/<id>. It sends your bearer token as Authorization: Bearer <token> on every socket upgrade and every HTTP read.
The client never reconnects. When any socket ends you get a closed event. Close the client and connect a new one, with backoff.
Note
covr turns gateway access on per desk and sends you the URL (wss://<provided by covr>) and your token. Until covr lists your desk on a market, your bid intents are verified and then refused with Bid intent verified but not entered: this market does not list this maker yet (practice mode).
Listeners are attached before each socket opens, so a frame the gateway sends while later feeds are still connecting is queued, never lost.
Warning
A bad token does not reject connect. The gateway completes the upgrade and then closes the socket with code 4401. You see it as closed events with code: 4401. Likewise 4403 (an asset your token does not allow) and 4429 (more than 16 sockets for your maker).
Throws (the promise rejects):
Condition
Error
wsUrl is not a URL
TypeError: Invalid URL
wsUrl is not ws: or wss:
maker-sdk: gateway URL must be ws:// or wss://
token is given but empty, not a string, or contains CR or LF
maker-sdk: invalid gateway token
An asset is neither a 0x + 40-hex alias nor a valid base58 mint
SolanaError from @solana/kit
An option is not an integer in its range
maker-sdk: <option> must be an integer 1..<max>
A socket fails before its upgrade completes (DNS, refused, TLS, a non-101 HTTP reply, handshake timeout)
the ws library's error, for example Opening handshake has timed out; every socket opened so far is terminated
Leaving the loop closes the client.break, return or a throw inside for await calls the iterator's return(), which calls close.
Ends after close. Events still queued when the client closes stay readable; then the stream ends.
Bounded. If more than maxQueuedEvents (default 1,000) events are unread, the client queues one last closed event with socket: 'client' and code: 1008, then closes every socket with 1008 event queue overflow. Handle events without blocking the loop.
Throws: nothing.
ts
importtype { GatewayClient } from'@covrfun/maker-sdk';
declareconstclient: GatewayClient;
forawait (const event of client.events()) {
if (event.type === 'closed') break;
}
Reads one RFQ's snapshot: market, writer wallet, deadlines and the request. Call it for every rfq event before you bid.
ts
getRfq(id: string): Promise<RfqSnapshot>;
Name
Type
Description
id
string
The RFQ id from the rfq event: a lowercase UUID v4.
Returns:RfqSnapshot. The gateway shows another maker's winning quote to no one but that maker.
Throws (the promise rejects):
Condition
Error
id is not a lowercase UUID v4
maker-sdk: invalid auction id
No response within httpTimeoutMs
maker-sdk: GET /api/rfqs/<id> timed out after <ms> ms
Network failure
the error fetch throws
The body is not JSON
the SyntaxError from parsing it
Non-2xx status
GatewayError with code = the HTTP status and message = the body's error (or RFQ read failed). The desk listener answers 401 Unauthorized, 404 RFQ not found (also for an asset your token does not allow), 404 Not found, 500 RFQ read failed and 503 Gateway stopping.
ts
importtype { GatewayClient } from'@covrfun/maker-sdk';
declareconstclient: GatewayClient;
declareconstid: string; // from an rfq eventconst snapshot = await client.getRfq(id);
if (snapshot.status !== 'collecting' || Date.now() >= snapshot.closesAt) {
// too late to bid
}
From signIntent. Sent as the params, with JSON-RPC id = intent.auctionId.
Returns:"BID_ACCEPTED": the gateway entered your intent in the auction.
Throws: the request errors. Gateway refusals reject with GatewayError (code: -32602), for example Bid intent verified but not entered: this market does not list this maker yet, Maker already bid in this auction or Auction collection closed.
Returns:"OK": the gateway admitted your quote and offers it to the customer. It is not a fill. The gateway sends no fill message: watch the chain for your position PDA (derivePosition).
Caution
If this rejects with OutcomeUnknownError, the quote may be live. Keep its nonce and cash holds until the chain settles it: a fill lands, or the chain clock passes validUntil. To kill it early, raise min_nonce above its nonce (configureMaker).
Throws: the request errors, or GatewayError for a refusal. Any failed answer from the award holder passes the award to the next bidder.
Passes the award you hold to the next bidder at once, as award_decline. Answer every award: a silent winner holds it until the hard deadline and the customer gets no quote.
The gateway accepts a note only on an ordinary RFQ, only from a maker that market lists, and only if no other maker has already noted that RFQ. In practice mode it is refused with RFQ note needs a maker this market lists.
Throws:maker-sdk: unknown rfq_note reason for a reason outside the list (nothing is sent); the request errors; or GatewayError.
Closes every socket and ends the event stream. Safe to call more than once.
ts
close(): void;
Returns: nothing.
Each socket gets a normal close handshake. One that has not finished closing 1 s later is terminated, on a timer that never keeps your process alive. Every request still waiting for its reply rejects with OutcomeUnknownError. No closed events are emitted after close().
sendBidIntent, sendQuote, sendAwardDecline and sendRfqNote share one request path. Each request's JSON-RPC id is the RFQ id (for rfq_note, a fresh UUID), and the gateway's reply carries the same id.
Refused before sending (plain Error, nothing reaches the gateway):
Condition
Message
The client is closed, or /maker is not open
maker-sdk: /maker socket is not open
The id is not a lowercase UUID v4
maker-sdk: invalid auction id
An earlier request for this RFQ timed out
maker-sdk: not sending <method> for <id>: the earlier <method> for it timed out and its outcome is unknown (its late reply would look like this one's). Treat that <method> as possibly accepted
A request for this RFQ is still waiting for its reply
maker-sdk: a request for this RFQ is already awaiting its reply
rejects with OutcomeUnknownError: maker-sdk: <method> for <id> timed out; its outcome is unknown: treat a signed quote as possibly live. No further request is sent for this RFQ
The socket failed while sending
rejects with OutcomeUnknownError: maker-sdk: <method> for <id>: the socket failed while sending it (<cause>); its outcome is unknown: treat a signed quote as possibly live (the send error is cause)
The /maker socket closed, you called close(), or the client closed itself on queue overflow
rejects with OutcomeUnknownError: maker-sdk: <method> for <id> got no reply before <why>; its outcome is unknown: treat a signed quote as possibly live. <why> is the /maker socket closed (plus : <reason> when the close carried one), the client closed, or the client closed itself (more than <n> events went unread). A feed socket closing fails no request.
Only a timeout blocks later requests. After one, the client refuses every further request for that RFQ id, because a late reply would look like the new request's reply. It remembers the last 4,096 such ids. A send failure or a close does not add the id; after a close every request fails anyway with maker-sdk: /maker socket is not open. A reply that arrives after its request timed out appears as a notice event.
A feed frame { id, result } whose id is an RFQ id and whose result is an object
asset: the feed's lowercase alias. id: the RFQ id. request: the RfqRequest, unvalidated.
award
A /maker frame with method: 'award', a string id, an intent object and integer awardUntil and hardDeadline
Award fields: id, intent (as the gateway sent it, unvalidated), awardUntil and hardDeadline in epoch ms.
notice
Anything else: a late reply, any other gateway push, or text that is not JSON
socket: where it came from. message: the parsed JSON, or the raw string.
closed
A socket ended: the gateway closed it, it errored after opening, or its heartbeat failed. Or the client closed itself on queue overflow.
socket ('client' for overflow), asset for a feed, code, reason.
closed codes you may see:
code
reason
Meaning
4401
Unauthorized
No token, or an unknown one. Sent right after the upgrade, before any RFQ.
4403
Asset not allowed
A feed for an asset your token does not allow.
4429
Too many connections
Your maker already holds 16 sockets across all its tokens.
4429
Too many frames
More than 100 frames at once, or more than 20 a second after that, on one connection.
1006
the error's message; no pong within <ms> ms; or empty
The socket errored after opening (the error's message), missed its heartbeat (no pong ...), or the connection dropped without a close frame (empty). The client ends the socket at once in the first two cases.
1008
maker-sdk: more than <n> events went unread; ...
socket: 'client': the event queue overflowed. Always the last event.
any other
the gateway's close reason, often empty
The gateway closed the socket.
Tip
Answer an award only when award.intent equals the intent you signed and stored for award.id, and build the quote from your copy, never from the award's.
A request reached an open socket and its reply never came: it timed out, the socket failed while sending it, /maker closed, or the client closed while it waited. The gateway may have applied it. A failed send is included because frames queued behind one write share its result.
The request's JSON-RPC id: the RFQ id, or the note's own id for rfq_note.
cause
unknown
The send error, when the socket failed while sending.
Treat a quote whose send ended this way as possibly live. Keep its holds until the chain settles it. Every other rejection is either a request refused before it was sent (plain Error) or the gateway's answer (GatewayError).
ts
import { OutcomeUnknownError, typeGatewayClient, typeQuoteV2 } from'@covrfun/maker-sdk';
declareconstclient: GatewayClient;
declareconstintentSignature: string;
declareconstquote: QuoteV2;
try {
await client.sendQuote(intentSignature, quote);
} catch (error) {
if (error instanceofOutcomeUnknownError) {
// Possibly live: keep nonce and cash holds until a fill lands or validUntil passes on chain.
} elsethrow error;
}
The client never reopens a socket. On any closed event, close it and connect a new client with capped exponential backoff and jitter. covr's example bot uses 1 s doubling to 30 s.
Upgrade timeout
10,000 ms
connect rejects if any socket's upgrade takes longer.
Heartbeat
ping every 15,000 ms
Each socket gets a WebSocket ping with 16 random bytes. Only a pong echoing those bytes counts.
Heartbeat timeout
30,000 ms
No matching pong within 30 s of the ping ends the socket: closed with code 1006, reason no pong within 30000 ms. A path that dies silently is reported within about 45 s.
Socket error after open
Ends the socket at once: closed with code 1006 and the error text.
Rejects with maker-sdk: GET /api/rfqs/<id> timed out after 5000 ms.
Inbound frame limit
16,384 bytes
A larger frame from the gateway ends that socket.
Close grace
1,000 ms
close() terminates a socket whose close handshake is still pending after 1 s.
The gateway pings every desk socket every 30 s and drops one that missed the previous ping or has more than 1 MiB of unread data queued. The ws library answers its pings automatically. Each connection counts toward your maker's 16-socket cap until the gateway sees it close, so close an old client before you connect a new one.
Every type the SDK exports, with each field's meaning and unit. Declarations are copied from the shipped .d.ts files. Types re-exported from ethers (TypedDataDomain, TypedDataField) and @solana/kit (Address, Instruction, AccountMeta) are those libraries' own.
Units used below:
Unit
Meaning
atoms
Collateral base units. JitoSOL has 9 decimals: 1 JitoSOL = 1_000_000_000 atoms.
USDC atoms
10^-6 USDC. 1 USDC = 1_000_000.
priceE18
USDC × 10^18 per one whole collateral token.
strikeE8
USD × 10^8 per whole collateral token.
quantityE18
Collateral atoms scaled to 18 decimals (the wire quantity).
Which program and deployment an operation targets. Every function that derives or decodes program accounts takes one as an optional last argument, default MAINNET. See the convention.
The instruction type the vault, budget, deposit, withdraw and exposure-limit builders return: a @solana/kit instruction whose accounts and data are always present. Other builders return the same shape under their own names.
A restricted market (settlement mode 1): every MarketV3 field, with settlementMode: 1 and a cancellation timeout. Restricted markets are covr-only today.
exportinterfaceCreateMarketInput {
creator: string;
salt: bigint;
collateral: string;
feeOwner: string;
/** Unix seconds. The program requires 60 s <= expiry - now <= 90 days at creation. */expiry: bigint;
window: bigint;
strikeE8: bigint;
maxQuantityAtoms: bigint;
feeBps: number;
/** Your clock in unix seconds, used to apply the program's expiry bounds. The chain
* clock decides; leave margin for submission delay. */nowUnix: bigint;
}
Field
Type
Meaning
creator
string
Wallet that signs and pays rent. PDA seed.
salt
bigint
Any u64. One market per (creator, salt).
collateral
string
Collateral mint. Not native SOL.
feeOwner
string
Owner of the USDC account that receives fees. Not the zero key.
expiry
bigint
Option expiry, unix s. 60 s to 90 days after the chain clock.
window
bigint
Exercise window, seconds, 60 to 3,600.
strikeE8
bigint
Strike, USD × 10^8. Positive.
maxQuantityAtoms
bigint
Largest single fill, atoms. Positive.
feeBps
number
Fee, basis points, integer 0 to 1,000.
nowUnix
bigint
Your clock, unix s. Used only for the local expiry check; it is not sent.
0 open (filled, not settled); 1 pending_delivery (restricted: strike funded, awaiting delivery); 2 exercised; 3 released (collateral returned after the window); 4 early_closed; 5 bilaterally_canceled (restricted cancellation).
timeoutAnchor
0 | 1
0 ordinary, 1 restricted.
settlementMode
string
'ordinary-atomic' or 'restricted-pending'.
collateralDecimals
number
Collateral mint decimals.
paymentDecimals
6
USDC decimals.
maker
Address
Your Maker account.
makerAuthority
Address
Your Solana authority.
makerEthAddress
string
Your quote key, lowercase.
writer
Address
The customer who sold the call.
termsAccount
Address
The market account.
termsId, profileHash, deploymentId
string
32-byte values as 0x hex, copied from the market.
genesisHash
Address
Mainnet genesis hash.
collateralMint, collateralProgram
Address
Collateral mint and token program.
paymentMint, paymentProgram
Address
USDC mint and SPL Token program.
policy, usage
Address
Your mint policy and mint usage accounts.
collateralVault
Address
Holds the writer's collateral until settlement.
strikeEscrow
Address
Restricted only: holds the funded strike.
nonce
bigint
The quote nonce that opened it.
admissionGeneration
bigint
Your mint policy generation when it was filled.
quantityAtoms
bigint
Collateral locked, atoms.
grossPremiumUsdc
bigint
Premium your vault paid, USDC atoms.
strikeUsdc
bigint
Strike payment due on exercise, USDC atoms.
openedAt
bigint
Fill time, unix s.
expiry
bigint
Option expiry, unix s.
exerciseWindowSeconds
bigint
Window after expiry, seconds.
exerciseDeadline
bigint
expiry + exerciseWindowSeconds, unix s. Exercise before it; release from it.
exportinterfacePositionReadInput {
/** PositionV3 and Maker account data from ONE finalized getMultipleAccounts read at `slot`. */position: {
address: string;
data: Uint8Array;
};
maker: {
address: string;
data: Uint8Array;
};
slot: bigint;
makerEthAddress: string;
/** Your own Solana authority, pinned independently of these bytes. */makerAuthority: string;
nonce: bigint;
/** The position's market (or its collateral identity). */collateral: Pick<DecodedMarketV3, 'collateral' | 'collateralProgram' | 'collateralDecimals'>;
/** Refuse unless both reads are at or after this slot. Defaults to `slot`. */
minContextSlot?: bigint;
writer?: string;
}
Field
Type
Meaning
position
{ address; data }
The position account and its 652 bytes.
maker
{ address; data }
Your Maker account and its bytes, from the same read.
slot
bigint
The context slot of that read.
makerEthAddress
string
Your quote key address.
makerAuthority
string
Your Solana authority, from your own config, not from the bytes.
exportinterfaceGatewayClientOptions {
/** Collateral mints or 20-byte aliases to subscribe to. Default: JitoSOL. */
assets?: readonlystring[];
/** Reply timeout for /maker requests. Default 5000 ms; the award window is 3000 ms. */
requestTimeoutMs?: number;
/** HTTP base for /api routes. Default: the WebSocket URL with ws->http, wss->https. */
httpUrl?: string;
/** getRfq gives up after this. Default 5000 ms. */
httpTimeoutMs?: number;
/** Most events held unread for events(). One more closes the client: the queued events
* stay readable, then a 'closed' event from 'client'. Default 1000. */
maxQueuedEvents?: number;
/** Each socket's upgrade must complete within this. Default 10000 ms. */
handshakeTimeoutMs?: number;
/** Every socket is pinged this often... Default 15000 ms (the heartbeat covr's own maker uses). */
heartbeatIntervalMs?: number;
/** ...and ended when the matching pong has not arrived within this. Default 30000 ms. */
heartbeatTimeoutMs?: number;
}
This section is the wire reference for covr's RFQ gateway, as a market-making desk sees it. It lists every route, every message in both directions, every limit and every error. Use it to build a client in any language, or to debug one.
Most desks do not need it. The SDK's GatewayClient speaks this protocol for you: it opens the sockets, sends your token, matches replies to requests, runs a heartbeat and turns refusals into typed errors. Read on if you write your own client, or if you want to know exactly what GatewayClient sends.
Note
Desk access is turned on per desk by covr. Until covr lists your desk on its markets, your bids are verified like real ones and then refused with Bid intent verified but not entered: this market does not list this maker yet. This is practice mode. It skips a few checks; see Practice mode for exactly what it tests. Practice bids never rank, never win and never change what a customer sees. Desks can quote JitoSOL markets only.
Your command socket. You send bid_intent, quote, award_decline and rfq_note. The gateway sends replies and award messages.
/rfqs/<asset alias>
WebSocket
gateway to desk
Read-only feed. One rfq event per new customer request for that collateral.
GET /api/rfqs/<id>
HTTPS
request, response
One RFQ's snapshot: market, customer wallet, deadlines, status, and your own admitted quote if you won.
No other route exists for desks. There is no market list, no order book, no position list and no quote withdrawal route. Each RFQ snapshot names its market account: read it from chain.
Warning
There is no withdrawal route for desks. For an immediate, reversible stop, pause your mint policy (Cancel quotes). To cancel quotes you have signed for good, raise min_nonce on your Maker account with configureMaker. Every fill whose nonce is below the new floor then fails on chain. A quote is valid for at most 60 s anyway.
Every upgrade and every HTTP request carries one header:
text
Authorization: Bearer <token>
covr issues the token. Tokens covr mints start with covr_mk_. Keep it secret.
A leaked token lets someone read your RFQ feed and RFQ snapshots, which name customer wallets. It lets them open sockets as your desk and take your 16 slots, so your real bot is refused with 4429. Once covr lists your desk, it also lets them set the customer-facing rfq_note reason on RFQs for your assets. It does not let them bid or quote for you: a bid intent must be signed by your registered quote key, and an award goes only to the socket that sent the winning bid. If a token leaks, ask covr to replace it.
The gateway stores only a SHA-256 digest of each token. It hashes the token you present and compares it with every registered digest in constant time. A token never appears in a close reason, an HTTP body or a JSON-RPC error.
A token is bound, at covr's side, to three things. None of them is taken from your frames.
Bound value
What it is
What it controls
Quote key
Your secp256k1 signing address (0x + 40 hex), the key you registered with register_maker_v3.
The maker field of every bid intent and quote you send must be this key.
Solana authority
The authority of your on-chain Maker account.
A fill of your quote pays from, and opens a position for, the Maker account of this key and authority.
Allowed assets
1 to 16 collateral mints. For desks today: JitoSOL only.
Which /rfqs/<alias> feeds you may open and which RFQs you may act on.
covr can have several tokens for one quote key (for example one per server). They all act as the same maker and share one connection cap. covr checks at startup that your Maker account exists on chain with the registered key and authority, so register on chain before you ask for a token.
The two WebSocket routes carry JSON text frames. The /maker socket uses JSON-RPC 2.0 with a few covr rules:
id is a string of at most 128 characters. For bid_intent, quote and award_decline it must be the RFQ id: the gateway looks the RFQ up by id. For rfq_note it is any string; the RFQ goes in params.rfqId.
params is an object. Exact field sets are given per method in Messages.
Every reply carries the request's id. A frame the gateway cannot parse gets a reply with "id": null.
Every refusal uses error code -32602, including parse errors. The message text tells you why. See Errors.
Batch requests (a JSON array) are not supported.
Replies to different RFQs can arrive in any order. Match them by id. Keep at most one request per RFQ in flight.
The gateway also pushes two unsolicited shapes:
Pushed on
Shape
Meaning
/rfqs/<alias>
{"jsonrpc":"2.0","id":"<rfq id>","result":{...}}
A new RFQ. It looks like a response, but you never sent a request.
This page covers how a desk opens, keeps and reopens its gateway sockets: the handshake, the bearer header, asset aliases, the heartbeat and reconnects.
The endpoint has room for another desk connection. The room is shared by all desks.
Connection dropped, or HTTP 503, before any handshake.
2
The path is /maker or /rfqs/<alias> of a served asset.
Connection dropped before any handshake.
3
The token is registered.
Handshake completes, then close 4401Unauthorized.
4
For a feed: the asset is in your token's allowlist.
Close 4403Asset not allowed.
5
Your maker has fewer than 16 open sockets.
Close 4429Too many connections.
A refused socket is never subscribed and never read. It sees no RFQ. If you do not answer the close frame, the gateway cuts the connection after about 1 s.
The feed path names a collateral by its 20-byte wire alias, not by its mint. The same alias appears in every RFQ (asset, collateralAsset) and in every bid intent and quote you sign (assetAddress, collateralAsset). The USDC alias appears as usd.
text
alias = "0x" + lowercase hex of the last 20 bytes of keccak256(32-byte mint public key)
An alias is an identifier only. It is not an EVM contract.
Every 30 s the gateway visits each of your sockets:
If the socket did not answer the previous ping, it is terminated.
If more than 1 MiB of outgoing data is queued for it (you stopped reading), it is terminated.
Otherwise it gets a new WebSocket ping with a fresh random 16-byte payload.
Only a pong that echoes the latest ping's payload exactly counts. An unsolicited pong, or an echo of an older ping, does not. RFC 6455 clients do this for you. So a socket whose peer vanished without closing is gone within two beats, about 60 s.
A terminated socket gets no close frame. Your client sees an abnormal closure (1006 in most libraries).
Pongs are free. Your own pings count against your frame budget.
Warning
While a dead socket waits to be reaped it still holds its slot and any award it holds. An award lasts at most 3 s, far shorter than the reap. A desk whose path dies silently while it holds an award loses that RFQ, and the customer gets no quote.
The SDK runs its own heartbeat on every socket, so it notices a dead path from its side too:
Setting
Default
Range
heartbeatIntervalMs: ping every
15 000 ms
1 to 300 000
heartbeatTimeoutMs: end the socket if no pong within
30 000 ms
1 to 300 000
handshakeTimeoutMs: each upgrade must finish within
10 000 ms
1 to 60 000
With the defaults a path that dies silently is reported within about 45 s: up to 15 s until the next ping, then 30 s for its pong. The socket then ends with a closed event whose reason says no pong within 30000 ms.
If you write your own client, do the same. A TCP connection through NATs and proxies can die without a FIN or RST, and nothing else will tell you.
When any of your sockets ends, treat the whole connection as gone:
Close every socket you still hold. This frees their slots at once. A socket you abandon without closing keeps its slot until the heartbeat reaps it.
Wait. Use capped exponential backoff with jitter. The example bot starts at 1 s, doubles to a 30 s cap, and picks a random delay in the upper half of each step.
Open /maker, then the feeds.
Forget bids you sent on the old connection. Keep reconciling quotes you signed: one may still fill.
Do not retry 4401 or 4403 in a loop. They mean your token or allowlist is wrong, and only covr can change that. 4429Too many connections usually means old sockets are still open: close them, or wait up to about 60 s for the heartbeat to reap dead ones.
This page lists every message a desk sends or receives, with a full example and a field table for each. For when each one happens, see RFQ lifecycle. For refusals, see Errors.
The examples use the SDK's golden vectors: a synthetic JitoSOL market (strike $170, expiry 2026-10-23 08:00 UTC, not on chain) and a throwaway quote key, privateKey = keccak256(utf8("covr-maker-sdk-golden-vectors-v1")). Their signatures verify, so you can test your own encoder against them.
65 bytes: 0x + 130 hex. Last byte 1b or 1c (v = 27 or 28). Low s.
RFQ id
string
Lowercase UUID version 4.
"3f2b8c1e-5d4a-4e7b-9c2d-1a2b3c4d5e6f"
The premium must come out as a whole number of USDC atoms (6 decimals):
text
premium (USDC atoms) = quantity × price / 10^30 must be an exact integer, > 0, < 2^64
For JitoSOL this is atoms × price / 10^21. Example: 0.5 JitoSOL at price = 2400000000000000000 pays 500000000 × 2.4×10^18 / 10^21 = 1200000 atoms = 1.20 USDC. A price that leaves a remainder is refused. The SDK's exactPriceAtOrBelow rounds your price down to the nearest exact one. See Settlement.
Once, at the moment a customer's request creates the RFQ. Sent to every open feed for that asset, practice-mode desks included.
Warning
The rfq event carries no market and no customer wallet. You need both to sign a bid. Call GET /api/rfqs/<id> as soon as the event arrives: bids close 3 s after the RFQ was created, and that read is part of your 3 s.
Winner only. The admitted quote, exactly as you sent it.
premiumBaseUnits
string
Winner only. Gross premium in USDC atoms. The market fee is taken out of it on chain.
collateralBaseUnits
string
Winner only. Collateral in atoms.
Sealed bids stay sealed. A desk that did not win never sees the winning price, maker or signature. The gateway may add fields later: ignore any you do not know.
Bids are closed. An award is out, a quote is being admitted, or a ranked bid is still to be offered. Before hardDeadline.
quoted
A quote was admitted. Final.
no_quote
No quote was admitted: no bids, every bidder declined or failed, or the hard deadline passed. Final.
Note
quoted stays quoted after the quote's validUntil. From then on the winner's read no longer carries quote, premiumBaseUnits or collateralBaseUnits. Whether the customer filled is visible only on chain.
After you read the snapshot, before closesAt. One per maker per RFQ.
JSON-RPC id
The RFQ id.
Reply
"BID_ACCEPTED"
A bid intent is your sealed bid: a price, signed, with no nonce and no cash commitment yet. The gateway ranks intents when collection closes and awards the best.
params has exactly these 18 fields. A missing or extra field is refused.
Name
Type
Description
auctionId
string
The RFQ id. Must equal the JSON-RPC id.
kind
string
"open".
market
string
Snapshot market.
position
null
null.
wallet
string
Snapshot wallet.
assetAddress
string
Request asset, copied exactly (lowercase).
chainId
number
Request chainId.
expiry
number
Request expiry.
isPut
boolean
Request isPut (false).
isTakerBuy
boolean
Request isTakerBuy (false).
quantity
string
Request quantity, copied exactly.
strike
string
Request strike, copied exactly.
usd
string
Request usd, copied exactly.
collateralAsset
string
Request collateralAsset, copied exactly.
maker
string
Your registered quote key. A key other than the one your token maps to is refused.
price
string
Your bid, priceE18. Positive, and the premium must be a whole number of USDC atoms (see Units). This is the price you must quote if you win.
validUntil
number
Unix seconds. Later than the gateway's clock, later than ceil(closesAt / 1000), and not later than expiry. The SDK default is min(expiry, ceil(hardDeadline / 1000) + 60).
signature
string
EIP-712 signature of the BidIntent typed data by maker, in the market's domain.
The instrument fields are compared as exact values, so copy them from the RFQ. Do not re-encode them.
Domain: {name: "SOL Covered Calls", version: "1", chainId: 2000000001, verifyingContract}, where verifyingContract is derived from the market account's terms. Get it with the SDK's marketDomain(marketAddress, accountData). See Markets and Signing.
Primary type BidIntent, fields in this order:
Field
EIP-712 type
Value signed
auctionId
bytes32
keccak256(utf8(rfqId))
kind
uint8
0 for "open"
market
bytes32
The market's 32-byte public key
position
bytes32
32 zero bytes for null
wallet
bytes32
The wallet's 32-byte public key
assetAddress
address
as sent
chainId
uint256
as sent
isPut
bool
as sent
strike
uint256
as sent
expiry
uint64
as sent
maker
address
as sent
price
uint256
as sent
quantity
uint256
as sent
isTakerBuy
bool
as sent
validUntil
uint64
as sent
usd
address
as sent
collateralAsset
address
as sent
The golden vector above hashes to digest 0xea259d586b8e39bea882c410e157308254bdfeee6c94201ad671369a8de9bb12 under verifying contract 0x21a8f3a5ea7cf85099905bcf8f065f1d148a80f3.
BID_ACCEPTED means your bid entered the auction. It is not an award. In practice mode the frame is refused with Bid intent verified but not entered: this market does not list this maker yet after the field, term, price and signature checks pass. That refusal tells you your encoding and signature are correct. It does not check the 3 s deadline or the one-bid rule: see Practice mode.
Your winning bid intent, as the gateway validated it, signature included.
params.awardUntil
number
Your quote must be admitted before this, unix ms. In practice it equals hardDeadline.
params.hardDeadline
number
The RFQ's hard deadline, unix ms.
Answer only an award whose intent equals the intent you signed and sent. Build your quote from your own stored copy, not from the copy in the award. If you will not quote, decline at once so the next maker gets the RFQ.
The quote is the executable order the customer signs against. It adds a nonce and a short validity to your winning price and binds both to this RFQ and this customer wallet.
Caution
Write the nonce, the full quote terms and the cash they can cost to durable storage before you sign. A reply can time out after the gateway admitted the quote. Treat any quote you signed as live until its validUntil has passed on chain.
Your registered quote key. Same as the awarded intent.
nonce
string
A u64 you have never used, below 2^64 - 1. It must be at or above your on-chain min_nonce, or the fill fails.
price
string
Exactly the awarded intent's price.
quantity
string
Request quantity.
isTakerBuy
boolean
false.
validUntil
number
Unix seconds. now < validUntil <= now + 60, where now is the gateway's clock when it starts admitting this quote (not the RFQ's creation time), and validUntil <= expiry. It must still be in the future when admission ends. Leave a few seconds of margin. The example bot uses its own now + 55.
usd
string
The USDC alias.
collateralAsset
string
The collateral alias.
signature
string
EIP-712 signature of the QuoteV2 typed data by maker, in the same market domain as the intent. Low s.
Primary type QuoteV2, fields in this order: auctionId (bytes32, keccak256(utf8(rfqId))), writer (bytes32, the wallet's 32-byte public key), assetAddress (address), chainId (uint256), isPut (bool), strike (uint256), expiry (uint64), maker (address), nonce (uint64), price (uint256), quantity (uint256), isTakerBuy (bool), validUntil (uint64), usd (address), collateralAsset (address). The golden vector above hashes to digest 0x1c85fe837a80e9fc414d54e78932acdf85f8d0ccb0cc97d934460892455fa958.
OK means the gateway admitted your quote and is showing it to the customer. It is not a fill. The customer may sign the fill before validUntil, or may not. The gateway sends no fill message. Watch your position PDA on chain. See What OK means.
The award passes at once to the next ranked bid. You cannot take it back. A refusedaward_decline passes nothing: you keep the award. Fix the frame and resend it before awardUntil, or the RFQ ends with no quote.
Any refused request gets one error reply on the socket that sent it:
json
{"jsonrpc":"2.0","id":"3f2b8c1e-5d4a-4e7b-9c2d-1a2b3c4d5e6f","error":{"code":-32602,"message":"Bid intent verified but not entered: this market does not list this maker yet"}}
Name
Type
Description
id
string or null
The request's id. null when the frame was not valid JSON-RPC, so no id could be read.
The gateway sends desks no message types other than the ones above.
GatewayClient reports any frame it cannot match as a notice event. In practice that is a late reply to a request that already timed out, or an error reply with "id": null.
/maker answers two leftover methods, positions and balances, with fixed empty values ([] and {"mode":"local-sandbox","balances":[],"fundingEnabled":false}). They carry no information about your desk. Do not use them.
This page follows one RFQ from the customer's request to the fill on chain, with the exact timings the gateway enforces. It also defines how bids are ranked, how an award passes from one maker to the next, and what an OK reply does and does not mean.
The award goes to the socket that sent the winning bid. It carries awardUntil: the time by which your quote must be admitted. It is min(award time + 3 s, hard deadline), so in practice it equals the hard deadline.
An award passes to the next ranked bid at once when any of these happens:
Event
Effect
You send award_decline and get DECLINED.
Passes at once.
Your award_decline is refused (wrong intentSignature, extra fields, wrong socket).
Nothing. You still hold the award. Resend exactly {intentSignature} before awardUntil.
Your awarded socket closes, for any reason. This includes a heartbeat reap, a 4429, and a frame over 16 KiB.
Passes at once.
Your awarded socket had already closed when collection ended.
Your bid is skipped.
You answer with a well-formed quote frame for that RFQ and the gateway refuses it, for any reason: bad params, another intent, a bad signature, a late quote, or the gateway being busy.
Passes at once, after the error reply.
Another maker sends a bad frame about your RFQ.
Nothing. Only the award holder's own frames can pass its award.
You send a frame that is not valid JSON.
Nothing. It names no RFQ.
Each bid is offered at most once. A pass never goes back to a maker who already had the award. When no ranked bid is left, the RFQ ends no_quote at once.
If the award holder neither quotes nor declines, it keeps the award until awardUntil, which is the hard deadline. No other maker is offered the RFQ, and the customer gets no quote. A socket that died without closing counts as silent until the heartbeat reaps it, which takes longer than an auction.
Warning
A silent winner costs the customer the trade. If anything stops you from quoting, decline.
The gateway admits your quote only if all of these hold when it reads the frame:
You hold the current award for this RFQ, on this socket, and awardUntil has not passed.
intentSignature is the signature of your awarded intent.
The quote's maker and price equal the awarded intent's.
auctionId is the RFQ id and writer is the customer wallet from the snapshot.
Every instrument field equals the RFQ: aliases, chainId, strike, expiry, quantity, isPut = false, isTakerBuy = false.
now < validUntil <= now + 60 and validUntil <= expiry, in unix seconds. now is the gateway's clock when it starts admitting this quote, not the RFQ's creation time. validUntil must also still be in the future when admission ends.
The nonce is a u64 below 2^64 - 1, and the gateway has not already admitted it in a different quote of yours that is still live.
The premium, quantity × price / 10^30, is a whole number of USDC atoms.
The EIP-712 signature recovers to maker in the market's domain.
Then a final check runs on covr's side. It re-verifies your signature and terms against covr's reviewed copy of the market, checks that your token's maker and Solana authority are the ones the fill will use, and checks the quote is still live. If this check fails, or does not finish before the hard deadline, the quote is refused with Ordinary V3 quote admission unavailable and the award passes.
Keep your clock in sync with NTP. The gateway decides every deadline with its own clock.
"OK" means one thing: the gateway admitted your quote and now shows it to the customer as the winning quote.
It does not mean:
A fill. The customer reviews the quote and may sign the fill transaction before validUntil, or may not.
A funds check. The gateway checks your signature and terms, not your USDC vault, exposure limits or min_nonce. Those are checked when the customer's transaction is prepared and again by the program. If they fail, the customer cannot fill and you are not told.
No fill message is sent, ever. To learn the outcome, watch the chain:
Filled: your position PDA for this nonce exists. Derive it with the SDK's derivePosition. See Setup.
Not filled: the PDA does not exist, the chain's clock has passed validUntil, and no transaction ever referenced the PDA. A missing account alone is not proof: a position can be filled and later closed.
Until one of these is proven at finalized commitment, keep the cash the quote could cost on hold.
A fill moves the premium out of your USDC vault. You cannot cancel one quote through the gateway. For an immediate, reversible stop, pause your mint policy (Cancel quotes). To cancel signed quotes for good, raise min_nonce with configureMaker: every quote with a lower nonce then fails on chain.
Until covr lists your desk on a market, you are in practice mode there. You receive every RFQ for your allowed assets, and you can send bid intents. Each bid is checked, then refused with Bid intent verified but not entered: this market does not list this maker yet. The refusal comes before the bid would enter the auction, so some checks never run.
What practice mode checks, exactly as for a listed desk:
Your token, the RFQ's asset against your allowlist, and that maker is your registered quote key.
The intent's 18 fields, field types and every instrument field against the RFQ.
auctionId, market and wallet against the RFQ and its snapshot.
validUntil: later than the gateway's clock, later than ceil(closesAt / 1000), not later than expiry.
Price exactness: the premium must be a whole number of USDC atoms.
The signature: encoding, recovery byte, and recovery to your key in the market's EIP-712 domain.
What practice mode does not check or exercise:
The 3 s collection deadline. A late practice bid still gets the practice refusal, not Auction collection closed.
The one-bid-per-maker rule. A repeated practice bid gets the practice refusal again, not Maker already bid in this auction.
Ranking, awards, quote, award_decline, nonces, quote admission and fills. You never receive an award.
rfq_note. It is refused with RFQ note needs a maker this market lists.
Your open feed also does not count as a maker listening, so it never hides the customer's warming_up message.
Use practice mode to prove your feed handling, snapshot reads, price rounding, intent signing and reconnects. Time your bids yourself against closesAt: practice mode will not tell you that a bid was late. Your quote path is first exercised once covr lists you.
Your quote key may hold 16 open sockets on the gateway at once. /maker sockets and feed sockets count alike. All tokens covr issued for the same quote key share the 16. The 17th upgrade completes only to be closed with 4429Too many connections. Other desks are not affected.
A socket's slot frees when it closes. A dead socket that never closed keeps its slot until the heartbeat reaps it, within about 60 s.
The endpoint also holds a fixed total number of desk connections, shared by every desk, and a fixed number of refusals in progress. When either is full, a new upgrade is dropped, or answered with HTTP 503, before any WebSocket handshake. Back off and retry.
At most 32 refusals (4401, 4403, 4429Too many connections) are in progress at once across all callers. Each lasts about 1 s at most. A caller past that is dropped without a close code.
Every message you send costs 1. Every WebSocket ping you send costs 1.
Pongs cost nothing, so answering the gateway's heartbeat is free.
When a frame arrives with less than 1 left, the gateway closes the socket with 4429Too many frames and reads nothing more from it. Frames already read are still answered.
A normal desk uses a tiny fraction of this: about 2 frames per RFQ it bids on, plus 1 ping every 15 s from GatewayClient. Never retry a refused request in a tight loop.
The gateway reads frames of up to 16 384 bytes. A larger frame closes the socket with 1009 (message too big). The largest legitimate desk frame, a quote, is under 1 KB (931 bytes for the golden vector).
GatewayClient refuses to send a frame over GATEWAY_MAX_FRAME_BYTES (16 384) and rejects the request locally. It also refuses frames over that size from the gateway.
Every 30 s the gateway terminates a socket that has more than 1 MiB of data queued for it, or that did not echo the previous ping. Read your sockets continuously, including feeds you are not using right now. See Heartbeat.
The gateway runs at most 4 quote admissions at a time, across all makers. A quote that arrives while all 4 are busy is refused with Ordinary V3 quote admission unavailable, and the award passes. An admission must also finish before the hard deadline.
Through covr's public endpoint, the gateway must answer within
5 s, else 502Gateway unavailable
GET /api/rfqs/<id> costs nothing against your frame budget. Call it once per RFQ, not in a polling loop: one read gives you everything you need to bid.
GatewayClient adds its own bounds. All are GatewayClientOptions in milliseconds or counts:
Option
Default
Range
What it bounds
requestTimeoutMs
5 000
1 to 60 000
Wait for a reply on /maker. On timeout the request rejects with OutcomeUnknownError, and the client sends nothing more for that RFQ.
httpTimeoutMs
5 000
1 to 60 000
getRfq.
handshakeTimeoutMs
10 000
1 to 60 000
Each socket upgrade.
heartbeatIntervalMs
15 000
1 to 300 000
Time between the client's pings.
heartbeatTimeoutMs
30 000
1 to 300 000
Time to wait for a pong before ending the socket.
maxQueuedEvents
1 000
1 to 1 000 000
Unread events. One more closes the client with code 1008.
The request timeout (5 s) is longer than the whole award window (3 s), so a timeout never tells you that you missed awardUntil. Check awardUntil yourself before you sign. If a quote reply never arrives, its outcome is unknown: treat the quote as live.
This page lists every refusal a desk can get from the gateway: close codes, handshake refusals, JSON-RPC errors per method, and HTTP errors from GET /api/rfqs/<id>. Each entry says what it means and what to do.
The gateway never sends text generated by its runtime or libraries, such as a failed property read or a JSON parser message. You get a fixed text instead:
You get
When
invalid JSON request
The frame is not valid JSON.
invalid request
A runtime error inside the gateway's checks, not a policy refusal. The desk-reachable causes are: in bid_intent, a market or wallet that is not a string; in quote, an assetAddress, usd, collateralAsset, maker or writer that is not a string, or a signature whose s is 2^255 or more.
The network check could not complete. Please try again shortly.
A string field that must be a Solana public key is not valid base58: market or wallet in a bid intent, writer in a quote. Despite the wording, this is not a network problem. Fix the field. Do not retry the same frame.
Policy refusals keep their exact wording, listed below.
No Authorization header, a malformed one, or a token covr does not know. Sent before the socket is subscribed or read.
Check the header format and the token. Do not retry in a loop: ask covr.
4403
Asset not allowed
A feed for an asset your token does not allow.
Subscribe only to your allowed assets. Ask covr to change the allowlist.
4429
Too many connections
Your quote key already holds 16 sockets, across all its tokens.
Close sockets you no longer use. Dead ones free within about 60 s. Then retry with backoff.
4429
Too many frames
This socket spent its frame budget: 100 at once, then 20 per second. Nothing more it sent is read.
Fix the sending loop. Reconnect with backoff.
1009
(empty)
A frame over 16 384 bytes.
Send smaller frames. A legitimate frame is under 1 KB.
1006
(no close frame)
The gateway terminated the socket: it did not echo the previous heartbeat ping, or more than 1 MiB was queued for it. Also seen when the gateway restarts, or when the network path fails.
Reconnect with backoff. Answer pings and read continuously.
1002, 1007
set by the WebSocket library
The frame broke the WebSocket protocol, or a text frame was not valid UTF-8.
Fix your WebSocket client.
GatewayClient reports two more closes of its own, as closed events:
Code
Reason
Meaning
1006
no pong within 30000 ms
The SDK's own heartbeat found the path dead.
1008
maker-sdk: more than 1000 events went unread; ... (socket client)
Your code stopped reading events(). The client closed itself.
Listed in the order the gateway checks them. The first failing check is the one you get.
Message
Meaning
What to do
Sealed bid intents are unavailable
The JSON-RPC id is not an RFQ the gateway knows: a typo, or an RFQ older than 120 s.
Use the RFQ id from the feed as id.
Maker is not registered for this asset
The RFQ's asset is outside your token's allowlist.
Bid only on your assets.
Bid intent maker is not this connection's registered maker
params is not an object, or its maker is missing, not a string, or not the quote key your token maps to (compared case-insensitively).
Send the intent object with your own registered key.
Ordinary V3 opening unavailable
New orders are paused for a moment while covr updates its market list.
Skip this RFQ. Bid on the next.
Unexpected or missing bid intent field
params does not have exactly the 18 intent fields.
Send exactly the 18 fields, no id or extras.
Auction ID mismatch
auctionId is not a UUID v4, or differs from the JSON-RPC id.
Use the RFQ id in both.
Auction kind mismatch
kind is not "open".
Send "open".
invalid request
market or wallet is not a string.
Copy the values from the snapshot.
The network check could not complete. Please try again shortly.
market or wallet is a string but not a valid base58 public key. Not a network problem.
Copy the values exactly.
market mismatch
market is not the snapshot's market.
Copy it from GET /api/rfqs/<id>.
wallet mismatch
wallet is not the snapshot's wallet.
Copy it from GET /api/rfqs/<id>.
Opening intent must not bind a position
position is not null.
Send null.
Invalid chainId
Not a non-negative safe integer.
Copy from the RFQ.
Invalid expiry
Not a non-negative safe integer.
Copy from the RFQ.
Invalid validUntil
Not a non-negative safe integer.
Send unix seconds as a JSON number.
Invalid option side
isPut or isTakerBuy is not a boolean.
Send false, false.
Invalid assetAddress
Not a valid 20-byte hex address. Mixed case must be a valid checksum.
Copy the lowercase alias.
Invalid maker
maker names your key but is written in mixed case with a wrong EIP-55 checksum.
Send it lowercase or correctly checksummed.
Invalid usd
Same, for usd.
Copy from the RFQ.
Invalid collateralAsset
Same, for collateralAsset.
Copy from the RFQ.
canonical unsigned integer required
quantity, strike or price is not a plain decimal string (no sign, leading zero, decimal point or exponent).
Send decimal strings.
integer overflow
quantity, strike or price is 2^256 or more.
Check your units.
quantity must be positive
quantity is "0".
Copy from the RFQ.
strike must be positive
strike is "0".
Copy from the RFQ.
price must be positive
price is "0".
Bid a positive price, or send rfq_note.
assetAddress mismatch
Differs from the RFQ's asset (case counts).
Copy it exactly.
chainId mismatch
Differs from the RFQ.
Copy it exactly.
expiry mismatch
Differs from the RFQ.
Copy it exactly.
isPut mismatch
Differs from the RFQ.
Copy it exactly.
isTakerBuy mismatch
Differs from the RFQ.
Copy it exactly.
quantity mismatch
Differs from the RFQ.
Copy it exactly.
strike mismatch
Differs from the RFQ.
Copy it exactly.
usd mismatch
Differs from the RFQ.
Copy it exactly.
collateralAsset mismatch
Differs from the RFQ.
Copy it exactly.
Bid intent expired
validUntil is at or before the gateway's clock.
Check your clock and your units (seconds).
Bid intent expires before collection closes
validUntil <= ceil(closesAt / 1000).
Use at least ceil(hardDeadline / 1000) + 60, capped at expiry.
Bid intent validity exceeds option expiry
validUntil > expiry.
Cap it at expiry.
Invalid signature encoding
signature is not 0x + 130 hex.
Send the 65-byte signature as hex.
Invalid signature recovery byte
The last byte is not 1b or 1c.
Use v = 27 or 28.
Invalid bid intent signature
The signature cannot be checked: malformed r/s, or a value that does not fit its EIP-712 type.
Re-sign with a standard EIP-712 signer.
Bid intent signature does not match maker
It recovers to another address: wrong domain, wrong field encoding, or wrong key.
Use the market's domain and check against the golden vectors.
premium precision loss
quantity × price is not a multiple of 10^30: the premium is not a whole USDC atom.
Round your price with exactPriceAtOrBelow.
token amount overflow
The premium is 2^64 USDC atoms or more.
Check your units.
Bid intent verified but not entered: this market does not list this maker yet
Practice mode. Every check above passed: your bid is well-formed and correctly signed, but covr does not list your desk on this market yet. The two checks below are not run in practice mode.
Nothing. This is the expected reply until covr lists you. See Practice mode.
Auction collection closed
The bid arrived after collection closed (3 s).
Bid faster. Read the snapshot as soon as the RFQ arrives.
unknown RFQ comes before the award check. For every message below it, a refusal sent on the socket that holds the award also passes the award to the next maker. You cannot try again. The one exception is Executable admission is already in progress: your first quote is still being admitted and keeps the award. A refusal sent on any other socket changes no award.
Message
Meaning
What to do
unknown RFQ
The JSON-RPC id is not an RFQ the gateway knows.
Use the RFQ id as id.
Awarded intent signature and quote required
params does not have exactly intentSignature and quote.
Send exactly those two fields.
Auction is still collecting
Sent before collection closed. You cannot hold an award yet.
Wait for award.
Executable quote is not from the awarded intent
No award is current for this RFQ (you never got one, it passed on or ended, or a quote was already admitted), or intentSignature is not the current award's signature. In a rare race it also means the award window ended between the gateway's checks.
Quote only for an award you hold, with its intent's signature, before awardUntil.
Executable ordinary quote is not from the awarded transport
You sent the quote on a different socket from the one holding the award.
Quote on the socket that sent the bid.
Ordinary V3 quote admission unavailable
The quote could not be admitted now: the gateway is running 4 admissions already, new orders are paused, the gateway is stopping, your socket closed, awardUntil passed, covr's final check refused the quote or did not finish before the hard deadline, or the quote expired while it was checked.
Nothing to fix in most cases. If it repeats, check your terms against Quote admission.
quote object required
quote is not an object.
Send the signed quote object.
Unsupported quote schema
The field set is wrong: a missing or extra field other than signature, or only one of auctionId and writer.
Send exactly the 16 fields.
OrdinaryV3: QuoteV2 required
quote has the older 14-field shape: every other field and signature, but no auctionId and no writer.
Add auctionId and writer, and sign the QuoteV2 type.
Invalid auction ID
auctionId is not a UUID v4.
Use the RFQ id.
The network check could not complete. Please try again shortly.
writer is a string but not a valid base58 public key. Not a network problem.
Copy the snapshot's wallet.
canonical unsigned integer required
nonce, price, quantity or strike is not a plain decimal string.
Send decimal strings.
integer overflow
nonce or strike is 2^64 or more, or price is 2^128 or more.
Check your units.
OrdinaryV3: nonce, price or e18 quantity mismatch
nonce is the reserved 2^64 - 1, price is "0", or quantity differs from the RFQ.
Fix that field.
OrdinaryV3: quote identity or side mismatch
writer is not the customer wallet, an alias is wrong, chainId is wrong, or isPut / isTakerBuy is true.
Copy them from the snapshot and the awarded intent.
OrdinaryV3: quote time or strike mismatch
strike or expiry differs from the market, or validUntil is not a positive integer at or before expiry.
Copy strike and expiry. Cap validUntil at expiry.
OrdinaryV3: signature length
signature is missing, or is not 0x + 130 hex.
Send the 65-byte signature as hex.
OrdinaryV3: signature recovery byte
The last byte is not 1b or 1c.
Use v = 27 or 28.
OrdinaryV3: fractional USDC obligation
The premium is not a whole number of USDC atoms.
Quote the awarded price. Bid only exact prices.
OrdinaryV3: USDC obligation overflow or zero
The premium is zero or 2^64 USDC atoms or more.
Check your units.
invalid request
assetAddress, usd, collateralAsset, maker or writer is not a string, or the signature's s is 2^255 or more.
Fix the encoder. Use a low-s signer.
OrdinaryV3: quote signature signer mismatch
The signature does not recover to maker.
Sign the QuoteV2 in the market's domain with your quote key.
Ordinary auction identity
auctionId is a valid RFQ id, but not this RFQ's.
Use the awarded RFQ's id.
Unlisted ordinary maker
maker is not the quote key your token maps to.
Quote with your own key.
Ordinary quote review deadline
validUntil is at or before the gateway's clock, or more than 60 s after it.
Use about now + 55, capped at expiry.
Ordinary V3 nonce already used
The gateway already admitted a different quote of yours with this nonce, and it is still live.
Never reuse a nonce.
Auction is closed
A race only: the hard deadline passed on the gateway's internal timer between its earlier checks and the start of admission. A quote after awardUntil normally gets Ordinary V3 quote admission unavailable, and one after an admitted quote gets Executable quote is not from the awarded intent.
Answer sooner.
Executable admission is already in progress
You sent another quote while your first one is still being admitted.
Send one quote per award and wait for its reply.
Executable quote does not match awarded intent
price differs from the awarded intent's price.
Quote exactly your bid price.
Ordinary V3 quote publication unavailable
The quote passed every check, but the gateway could not publish it to the customer. Rare.
The decline was not accepted. Causes: unknown RFQ; no current award (never yours, passed on, ended, or already quoted); the award is held by another socket; params is not exactly {intentSignature}; or intentSignature is not the awarded intent's.
If you hold the award on this socket, resend exactly {"intentSignature": "<your awarded intent's signature>"} before awardUntil. Otherwise there is nothing to decline.
Executable admission is already in progress
You already sent a quote and it is being admitted.
Wait for the quote's reply.
A refused award_decline never passes the award. Unlike a refused quote, it leaves you holding the award, as a silent winner, until you send a correct decline or a quote, or until awardUntil passes. The customer then gets no quote.
No token, or an unknown one. Depending on how covr routes your endpoint, also a request that is not a GET or whose path is not exactly /api/rfqs/<lowercase uuid>.
Check the header, the method and the path.
404
{"error":"Not found"}
Depending on how covr routes your endpoint: not a GET, or the path is not exactly /api/rfqs/<lowercase uuid> (a query string, a trailing slash, uppercase hex). This check runs before the token check.
Fix the request.
404
{"error":"RFQ not found"}
No such RFQ, it is older than 120 s, or its asset is outside your allowlist.
Read the snapshot as soon as the RFQ arrives.
500
{"error":"RFQ read failed"}
The gateway failed to build the snapshot.
Skip this RFQ.
502
{"error":"Gateway unavailable"}
covr's endpoint could not get an answer from the gateway within 5 s.
Skip this RFQ. Retry later reads.
503
{"error":"Gateway stopping"}
The gateway is restarting.
Reconnect with backoff.
GatewayClient.getRfq turns a non-2xx answer into a GatewayError whose code is the HTTP status and whose message is the body's error. If no answer arrives within httpTimeoutMs (5 s), it rejects with maker-sdk: GET /api/rfqs/<id> timed out after 5000 ms.
This section is the reference for covr's covered-call program on Solana mainnet, as a market-making desk uses it. It lists every account your desk owns or reads, every instruction you sign or depend on, and every error you can get back.
It keeps your USDC in one vault that only the program can move, and it records each call you buy as a position account.
When a customer fills one of your signed quotes, it pays the premium from your vault (the customer gets it minus the market fee) and locks the customer's collateral (JitoSOL) in a vault owned by the position.
From expiry until the exercise window closes, your authority or operator can exercise: the program pays the strike from your vault to the customer and sends the collateral to your authority's token account.
Once the window has closed without an exercise, anyone can release the collateral back to the customer, and your position ends with nothing returned.
Every fill is checked on chain against your quote signature, your nonce floor and the limits you set on your own accounts.
Note
Desks receive covr's RFQs and can bid on them today, but the gateway does not let a desk win yet. Until covr lists your desk on its markets, your bids are verified and then refused ("practice mode"). The on-chain rules on these pages apply from your first real fill.
Every fixed address and identifier the program and the SDK use on Solana mainnet. Each value is exported by @covrfun/maker-sdk, except where a row says otherwise.
The covered-call program. Owner of every Maker, market and position account. Every PDA on these pages is derived under it.
GENESIS_HASH
5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d
Solana mainnet's genesis hash. The program writes it into every MarketV3 and PositionV3 and refuses records that carry another. Check your RPC serves this chain.
A 32-byte identifier of this deployment. It is stored in every MarketV3 (bytes 237 to 269) and PositionV3, and it is mixed into every EIP-712 verifyingContract.
MAINNET
{ programAddress, deploymentId, genesisHash }
The default last argument of every SDK function that derives an address or a signing domain.
The only payment mint. Premium, fee, strike, deposits and withdrawals are all USDC.
USDC_DECIMALS
6
1 USDC = 1,000,000 USDC atoms. The program requires exactly 6.
JITOSOL_MINT
J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn
The collateral desks can quote today. 9 decimals.
JITO_STAKE_POOL
Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb
Read-only extra account on every JitoSOL fill. The program reads its pool totals to cap one order at 2 SOL of stake-pool backing.
Stake pool program (no SDK constant)
SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy
The program refuses a stake pool account owned by any other program.
TOKEN_PROGRAM
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
SPL Token. Owns USDC, JitoSOL, your USDC vault and the position collateral vaults of JitoSOL markets.
TOKEN_2022_PROGRAM
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Token-2022. A market's collateral may use it; USDC never does.
The program refuses native SOL as collateral, in both token programs: So11111111111111111111111111111111111111112 and 9pan9bMn5HatX4EJdBwg9VgCa7Uz5HL8N1m5D3NdXejP.
The terms_id of restricted markets with the 7-day cancellation timeout. Restricted (tokenized-stock) markets stay covr-only for now.
RESTRICTED_CANCELLATION_TIMEOUT_SECONDS
604800
The only cancellation timeout the program accepts (7 days).
JITOSOL_ALIAS
0x43127405c3191a0b63a5f3d845f616ffea348c68
20-byte wire alias of JitoSOL: the last 20 bytes of keccak256(mint bytes). Not an EVM contract.
USDC_ALIAS
0x5ce3bf3a31af18be40ba30f721101b4341690186
The same alias for USDC.
Each market has its own quote domain. Its verifyingContract commits to every immutable byte of that market, so build it with quoteDomain from the decoded account, never by hand.
All the commands below only read. Point them at your own RPC.
1. Confirm the chain. The genesis hash must equal GENESIS_HASH.
sh
solana genesis-hash --url https://<your rpc>
2. Inspect the program. This prints the loader, the ProgramData address, the upgrade authority and the slot of the last deployment.
sh
solana program show 8ZpbTPnDECPTDGN5VHHKH5gfer6EUTnRHcFwfUAbkJDg --url https://<your rpc>
The program is upgradeable. At the time of writing, covr's own tooling expects the mainnet upgrade authority to be AhwMUppQXVfJRjn4sbb5Kpb67tee1z6qSSzEkRkjXpQS. If solana program show prints another authority, stop and ask covr before you fund a vault.
Warning
An upgrade can change any rule on these pages, including how your vault is spent. Record the last-deployed slot when you go live and alert on any change.
3. Check an account's owner. Every Maker, market and position account must be owned by PROGRAM_ID. Your USDC vault and the position collateral vaults are token accounts, owned by a token program.
The SDK decoders check discriminators, sizes and PDAs, but not the owner. Read accounts at finalized commitment (a pause or update may read the policy generation at confirmed: a stale value only fails with 6012) and check owner === PROGRAM_ID yourself before you call decodeMaker, decodeMintPolicy, decodeMintUsage, decodeMarketV3 or decodePositionV3.
Offsets are byte offsets into the raw account data. Bytes 0 to 8 are the Anchor discriminator: the first 8 bytes of sha256("account:<Name>").
Integers are little-endian. u64 and i64 take 8 bytes. bool is 1 byte (0 or 1). A Pubkey is 32 bytes.
Seeds are listed in order. A string seed is its UTF-8 bytes. A Pubkey seed is its 32 bytes. "Quote key" means the 20 raw bytes of your 0x… quote key address. A nonce or salt seed is a u64 in 8 little-endian bytes.
decodeMaker returns authority, ethAddress, bump, operator and minNonce, plus two fields that mirror bytes 101 to 117. For a Maker created by register_maker_v3 those two are always true and the registration slot.
It only goes up.configure_maker refuses a lower value with InvalidTerms (6001).
Raising it cancels at once. Every signed quote, early-close quote and sale quote with a lower nonce stops working on chain. Positions already open are not touched.
Each quote nonce opens at most one position. The position address is derived from your quote key and the nonce, across all markets. A second fill with the same nonce fails because the account already exists.
u64::MAX retires this quote key forever. No nonce passes a floor of 18446744073709551615 (u64::MAX), the floor can never go down, and a quote key can never register again. Setting it is permanent.
It also gates rent cleanup. A customer can close a settled position account only once your min_nonce is above that position's nonce. This does not move any of your funds.
Caution
Setting min_nonce to u64::MAX retires this quote key forever: it can never register again. To cancel everything outstanding, raise min_nonce to the highest nonce you have signed plus 1. To stop all fills on a mint for a while, use pause_mint_policy_v1 instead: it is reversible. If your quote key leaked, u64::MAX is the right move: see If your quote key leaks.
An optional lifetime cap on what fills can spend from your vault. One per Maker, shared by every collateral mint and market. Created by initialize_vault, disabled.
Seeds
"premium-budget", Maker, USDC mint
Size
90 bytes
SDK
deriveVault(maker).premiumBudget. No decoder.
Offset
Field
Type
Meaning
8
maker
Pubkey
Your Maker.
40
stable
Pubkey
USDC mint.
72
limit
u64
Cap on spent, in USDC atoms. Enforced only when enabled.
80
spent
u64
Lifetime gross premium (fee included) of all your fills, plus the price of every position you bought, in USDC atoms. Never goes down.
88
enabled
bool
false: no cap at all. true: a fill fails if spent + gross > limit.
89
bump
u8
PDA bump.
Note
A new vault has no premium cap. enabled is false until you call set_premium_budget.
Starts at 1. Each update or pause adds 1. Both must name the current value.
212
watcher
Pubkey
Optional key that may pause this policy. All zeros means none.
244
enabled
bool
false: every new fill on this mint fails.
245
terms_id
32 bytes
Must equal the market's terms_id. ORDINARY_TERMS_ID for ordinary markets.
277
profile_hash
32 bytes
Must equal the market's profile_hash (a fingerprint of the collateral mint).
309
max_order_atoms
u64
Largest single fill, in collateral atoms.
317
max_reserved_atoms
u64
Cap on collateral atoms open on this mint.
325
max_reserved_strike_usdc
u64
Cap on the total strike you owe on open positions on this mint, in USDC atoms.
333
lifetime_premium_limit_enabled
bool
Turns the next field on.
334
lifetime_premium_limit_usdc
u64
Cap on lifetime gross premium on this mint, in USDC atoms. Must be 0 when disabled.
decodeMintPolicy returns these fields with generation as a bigint and the settings under settings. It checks the PDA, bump, maker, authority and collateral identity, but not the account owner: read the account yourself and check owner === PROGRAM_ID first.
Every cap is checked at fill time, against the totals after the fill. A breach fails the fill with TokenLifecycleAccounting (8302). The policy is never read by exercise, release or early close, so pausing or tightening it never blocks an exit.
A policy holds one terms_id, so each mint is quoted either in ordinary markets or in restricted markets, never both. Restricted (tokenized-stock) markets stay covr-only for now, so a desk policy holds ORDINARY_TERMS_ID.
To stop every fill on a mint at once, pause the policy with pause_mint_policy_v1 (SDK: pauseMintPolicyV1). Your authority or the watcher key can sign it, and your authority can undo it with update_mint_policy_v1 (SDK: updateMintPolicyV1).
The running totals your mint policy is checked against. One per Maker and collateral mint. Created next to the policy, and unusable until bootstrap_mint_usage_v1 runs.
reserved_strike_usdc is the USDC you owe on that mint if every open position is exercised. Your vault is shared by all mints. Add this field across your mints and compare it with your vault balance.
The wallet that created the market. It alone can pause it.
45
collateral
Pubkey
Collateral mint.
77
collateral_program
Pubkey
Token program of the collateral.
109
payment
Pubkey
USDC mint.
141
payment_program
Pubkey
SPL Token program.
173
fee_owner
Pubkey
Owner of the USDC account that receives the fee. Never zero.
205
genesis_hash
Pubkey
Mainnet genesis hash.
237
deployment_id
32 bytes
Equals DEPLOYMENT_ID.
269
terms_id
32 bytes
ORDINARY_TERMS_ID or RESTRICTED_TERMS_ID.
301
profile_hash
32 bytes
Fingerprint of the collateral mint at creation, including its mint and freeze authorities.
333
salt
u64
Creator's salt.
341
expiry
i64
Expiry time, unix seconds.
349
window
u64
Exercise window after expiry, in seconds. 60 to 3,600.
357
strike_e8
u64
Strike in USD per whole token, times 10^8.
365
max_quantity_atoms
u64
Largest single fill, in collateral atoms. Not a cap on the market's total.
373
fee_bps
u16
Fee taken from the gross premium, in basis points. At most 1,000.
375
paused
bool
true: new fills fail. Exits still work.
If the collateral mint changes after a market is created (for example a new mint authority), the stored profile_hash no longer matches and new fills on that market fail with MarketV3Identity (8201).
Time rules for an ordinary position, with deadline = expiry + exercise_window_seconds:
Clock
Allowed
opened_at <= now < expiry
Early close by the writer, transfer by the writer.
expiry <= now < deadline
exercise_v3 by your authority or operator.
now >= deadline
release_v3 by anyone.
decodePositionV3 also returns status (open, pending_delivery, exercised, released, early_closed, bilaterally_canceled), exerciseDeadline, and your Maker's makerOperator and makerMinNonce from the same read.
Holds the customer's collateral while the position is open. Created by fill_v3. Exercise, release and early close empty it and close it. Its rent goes to the writer's collateral token account.
Seeds
"collateral", PositionV3
Owner
The market's collateral token program. Its token authority is the PositionV3 PDA.
Every program instruction a desk signs, pays for, or depends on. For each one: who signs, the accounts in order, the arguments with units, what the program checks, what money moves, and which SDK builder emits it.
Accounts are listed in the exact order the instruction expects. w = writable. s = signer.
Arguments are Borsh-encoded after an 8-byte discriminator, sha256("global:<instruction name>")[0..8]. Integers are little-endian. The SDK builders do this for you.
Compute gives the SDK's recommended compute-unit limit where one exists. Prepend a SetComputeUnitLimit instruction with it, and simulate the whole transaction first. These are tested envelopes, not guarantees.
Errors name the codes you are most likely to see. Errors has the full list.
These are the rules that decide where your USDC goes. Each one is enforced by the program, not by covr's servers.
1. A fill needs only the premium in your vault. At fill, the program moves the gross premium out of your vault: the customer gets gross - fee, and the market's fee owner gets fee. Nothing else leaves, and nothing is set aside for the strike.
2. The strike is paid from your vault at exercise.exercise_v3 moves the position's full strike_usdc from your vault to the writer, and the full quantity_atoms of collateral to a token account owned by your authority. Both legs succeed or the whole transaction reverts.
3. If your vault cannot pay the strike, exercise fails. The program checks the vault balance before it moves anything and fails with CustodyInvalidAmount (7405). Nothing moves. You can top up and retry while the window is open. There is no partial exercise and no grace period.
4. After the window, anyone can give the collateral back. From expiry + window on, release_v3 needs no signature from you. It returns all the locked collateral to the writer and closes the position as released. Your premium is not returned.
5. The exercise window is short and fixed. Every market has a window of 60 to 3,600 seconds after expiry. You can exercise when expiry <= now < expiry + window, by the Solana clock. Not before, not after.
6. All positions on a market expire together. Each position copies its market's expiry and window at fill time. Every position you hold on one market must be exercised inside the same window.
7. Withdrawals do not look at your open positions.withdraw moves any amount your vault holds to your authority's USDC account. The program does not stop you from withdrawing the USDC you need for upcoming exercises.
8. The program does not read a price. It lets you exercise at any spot price. Deciding whether to exercise is up to you.
Warning
Keep each exercise's strike in the vault before its window opens, and set the compute-unit limit on every exercise transaction. If no exercise succeeds before the window closes, the call is lost: anyone can then release the collateral to the customer, and you get nothing back for the premium you paid.
The quote key's EIP-712 signature over Register(bytes32 authority), where authority is the 32 bytes of the signing Solana key. r ‖ s ‖ v, v 27 or 28, low s.
Checks. The signature must recover to eth_address in the registration domain: { name: "SOL Covered Calls", version: "1", chainId: 2000000001, verifyingContract }, where verifyingContract is the last 20 bytes of keccak256("deployment-v2" ‖ DEPLOYMENT_ID ‖ PROGRAM_ID ‖ "registration-v3"). Otherwise BadSignature (6000). The Maker must not exist yet.
Effects. Creates a 117-byte Maker with your authority, no operator and min_nonce = 0, and a 179-byte origin record. No instruction can change the authority or the quote key afterwards.
Checks. Only the signer, the accounts and amount > 0. The program does not compare amount with the strike you owe on open positions: USDC you withdraw is no longer there to pay exercises. If the vault holds less than amount, the SPL Token program fails the transfer.
ts
import { deriveMaker, withdraw } from'@covrfun/maker-sdk';
declareconstmakerEthAddress: string; // your quote key addressdeclareconstauthority: string; // your Solana authority: the only key that can signdeclareconstdestination: string; // a USDC token account owned by your authorityconst { maker } = awaitderiveMaker(makerEthAddress);
const { instruction } = awaitwithdraw({ authority, maker, destination, amount: 250_000_000n }); // 250 USDC
The instruction is the discriminator b712469c946da122 followed by amount as a little-endian u64, with the six accounts above: the same accounts as deposit, with the destination in place of the source.
Changes the limit of an existing Exposure account.
Signer
authority
SDK
setExposureLimit({ authority, makerEthAddress, collateral: { mint, program, decimals }, limitAtoms }), the same input as initializeExposureV3. Returns { maker, exposure, instruction }. It refuses Token-2022 collateral before building, because the program would refuse it on chain.
#
Account
w
s
Notes
0
authority
s
Must equal Maker.authority.
1
maker
2
collateral
The collateral mint. Must be an SPL Token mint (JitoSOL is).
3
exposure
w
Seeds "exposure", maker, mint.
Name
Type
Description
limit
u64
New cap, in collateral atoms.
Effects. Replaces limit. A limit below active blocks new fills on that mint. It never blocks an exit. Discriminator: 21859ba43e8e7723.
Key that may pause this policy. Zero key for none.
enabled
bool
false blocks new fills on this mint.
terms_id
[u8; 32]
Market terms you accept. ORDINARY_TERMS_ID for ordinary markets. Not zero.
profile_hash
[u8; 32]
The profile_hash of the markets you will quote on this mint. Not zero.
max_order_atoms
u64
Largest single fill, collateral atoms.
max_reserved_atoms
u64
Cap on open collateral on this mint, collateral atoms.
max_reserved_strike_usdc
u64
Cap on total strike owed on open positions on this mint, USDC atoms.
lifetime_premium_limit_enabled
bool
Turns on the next field.
lifetime_premium_limit_usdc
u64
Cap on lifetime gross premium on this mint, USDC atoms. Must be 0 when disabled.
Checks.terms_id and profile_hash not zero, and a disabled lifetime limit set to 0, else MintPolicySettings (6014). The program does not check that they match a real market. If they do not, every fill on this mint fails with TokenLifecycleAccounting (8302). Take both from a decoded market.
Effects. Policy generation = 1. Usage starts not bootstrapped: run bootstrap_mint_usage_v1 next.
updateMintPolicyV1({ authority, maker, collateral, expectedGeneration, settings }). Returns { policy, usage, instruction }. Take expectedGeneration and the current settings from decodeMintPolicy.
#
Account
w
s
Notes
0
authority
s
Must equal Maker.authority.
1
maker
2
collateral
The collateral mint. Its token program and decimals must not have changed.
3
payment
USDC.
4
policy
w
5
usage
Name
Type
Description
expected_generation
u64
Must equal the policy's current generation, else MintPolicyGeneration (6012).
settings
MintPolicySettingsV1
Same 130-byte layout as above.
Effects. Replaces all settings and adds 1 to generation. Open positions keep their terms. Discriminator: 5a58a12379e8aa18.
The fast, reversible brake. One small instruction stops every new fill on one collateral mint, including fills of quotes you already signed. Exits are not affected: you can still exercise, and anyone can still release.
Your authority, or the policy's watcher key. Set a watcher in the policy settings to give a risk system a key that can pause but can change nothing else. Anyone else gets MintPolicyUnauthorized (6013).
Effect
Sets enabled = false and adds 1 to generation. Every later fill on that mint fails with TokenLifecycleAccounting (8302) and the log line COVR_TOKEN_ACCOUNTING: Paused.
Undo
update_mint_policy_v1 with enabled = true. Only your authority can sign it, and it needs the new generation.
Still works when
The collateral mint is frozen or changed: the instruction does not read the mint.
#
Account
w
s
Notes
0
authority
s
Your authority or the watcher.
1
maker
Your Maker.
2
policy
w
Seeds "mint-policy", maker, collateral mint.
Name
Type
Description
expected_generation
u64
Must equal the policy's current generation (u64 at byte 204 of the policy account). Otherwise MintPolicyGeneration (6012).
expected_generation makes each pause and update apply to the version you read. If another pause or update landed first, yours fails with 6012: re-read generation and resend. A pause of an already-paused policy still succeeds and adds 1 to generation.
ts
import { createSolanaRpc } from'@solana/kit';
import {
PROGRAM_ID, JITOSOL_MINT, TOKEN_PROGRAM,
deriveMaker, deriveMintPolicy, decodeMintPolicy, pauseMintPolicyV1, updateMintPolicyV1,
} from'@covrfun/maker-sdk';
declareconstmakerEthAddress: string; // your quote key addressdeclareconstauthority: string; // your authority (the policy records it)declareconstsigner: string; // your authority, or the policy's watcher keyconst rpc = createSolanaRpc('https://<your rpc>');
const collateral = { mint: JITOSOL_MINT, program: TOKEN_PROGRAM, decimals: 9 };
const { maker } = awaitderiveMaker(makerEthAddress);
const { policy } = awaitderiveMintPolicy(maker, collateral.mint);
asyncfunctionreadPolicy() {
const { value } = await rpc.getAccountInfo(policy, { commitment: 'confirmed', encoding: 'base64' }).send();
if (!value || value.owner !== PROGRAM_ID) throwError('mint policy not found');
returndecodeMintPolicy(policy, Buffer.from(value.data[0], 'base64'), { authority, maker, collateral });
}
// Brake: stop every new fill on JitoSOL.const before = awaitreadPolicy();
const pause = awaitpauseMintPolicyV1({
signer, maker, collateral: collateral.mint, expectedGeneration: before.generation,
});
// Later, undo (authority only): same settings with enabled = true.const paused = awaitreadPolicy();
const resume = awaitupdateMintPolicyV1({
authority, maker, collateral: collateral.mint,
expectedGeneration: paused.generation,
settings: { ...paused.settings, enabled: true },
});
Read the policy at confirmed for speed: a stale generation only costs a retry (6012). The instruction is the discriminator d84b57d2678c345f followed by expected_generation as a little-endian u64, with the three accounts above. In raw bytes, generation is the u64 at byte 204 of the policy account and enabled is byte 244.
No arguments. Checks. The origin must match your Maker and this deployment (MakerOriginSchema 8000, MakerOriginIdentity 8001). The usage must be untouched: not bootstrapped and every counter 0, else MakerOriginState (8002).
Any value. Makes the market address unique per creator.
fee_owner
Pubkey
Owner of the USDC account that receives the fee. Not the zero key.
expiry
i64
Unix seconds. 60 s to 90 days (7,776,000 s) after the chain clock at creation.
window
u64
Exercise window, seconds. 60 to 3,600.
strike_e8
u64
Strike, USD per whole token times 10^8. Above 0.
max_quantity_atoms
u64
Largest single fill, collateral atoms. Above 0.
fee_bps
u16
Fee on the gross premium, basis points. 0 to 1,000.
Checks. Terms as above, else MarketV3Terms (8202). Payment must be USDC, else MarketV3Identity (8201). The collateral must be a plain 82-byte mint (no Token-2022 extensions), with no freeze authority, and not native SOL, else MarketV3Unsupported (8203).
Effects. Stores the terms, terms_id = ORDINARY_TERMS_ID and a profile_hash fingerprint of the mint. Nothing can change them later except paused.
Pauses or resumes new fills on a market you created. No SDK builder. For a restricted market, use set_restricted_market_v3_pause with the same accounts.
#
Account
w
s
Notes
0
creator
s
Must equal MarketV3.creator.
1
market
w
Name
Type
Description
paused
bool
true blocks new fills. Exercise and release still work.
Opens a position from one of your signed quotes. The customer signs and submits it, not you. covr's gateway prepares it. Use the SDK builder to simulate or audit the exact instruction. For a restricted market the instruction is fill_restricted_v3, with one more argument.
Signer
writer (the customer). Pays rent for the position and its collateral vault.
SDK
fillV3(input). Returns the amounts and { addresses, recommendedComputeUnitLimit, instruction }.
Compute
300,000
#
Account
w
s
Notes
0
writer
w
s
The customer. Must equal the writer in your signed quote.
1
market
MarketV3.
2
maker
Your Maker.
3
position
w
Created. Seeds "position", quote key, nonce.
4
collateral_vault
w
Created. Seeds "collateral", position.
5
collateral
Collateral mint. Must equal market.collateral.
6
payment
USDC.
7
writer_collateral
w
Customer's collateral token account.
8
writer_payment
w
Customer's USDC account. Receives gross - fee.
9
fee_tokens
w
USDC account owned by market.fee_owner. Receives fee.
10
payment_vault
w
Your USDC vault. Pays the gross premium.
11
collateral program
Must equal market.collateral_program.
12
payment program
SPL Token.
13
system program
14
exposure
w
Your Exposure for this mint.
15
premium_budget
w
Your PremiumBudget.
16
policy
Your MintPolicyV1 for this mint.
17
usage
w
Your MintUsageV1 for this mint.
18
origin
Your MakerOriginV1.
19
JitoSOL stake pool
Remaining account. Only on JitoSOL fills, and then required.
Name
Type
Description
auction_id
[u8; 32]
The RFQ's auction id, as signed in your quote. Not zero.
nonce
u64
Your quote nonce.
quantity_atoms
u64
Collateral, in collateral atoms.
price_e18
u128
Your price: USDC per whole token, times 10^18.
valid_until
i64
Your quote's expiry, unix seconds.
signature
[u8; 65]
Your quote key's QuoteV2 signature in this market's quote domain.
cancellation_timeout_seconds
u64
fill_restricted_v3 only. Must be 604800.
Checks, in order.
auction_id is not zero (InvalidOrder 6003).
The market is a canonical V3 market, and the collateral mint still has the profile it had at creation (8200 to 8203).
The Maker is your canonical Maker account (7600, 7602), and the origin account is its MakerOriginV1 (8700, 8701). There is exactly one remaining account on JitoSOL fills and none otherwise; a missing or extra account fails with 8800. On JitoSOL, that account must be the pinned stake pool, read-only, updated this epoch (InvalidJitoPool 6009), and the order must pass the JitoSOL size cap (JitoSolOrderLimit 6008).
The payment mint is USDC (TokenLifecycleIdentity 8300).
The market is not paused. now < expiry and now < valid_until <= expiry. nonce >= min_nonce and nonce != u64::MAX. 0 < quantity_atoms <= max_quantity_atoms. Otherwise InvalidOrder (6003).
Your exposure, budget, policy and usage accounts are the canonical ones for this mint (TokenLifecycleIdentity 8300, or a MintPolicy error 6010 to 6014).
The signature recovers to your quote key over QuoteV2, bound to this auction id and this writer (BadSignature 6000).
Your mint policy accepts it: bootstrapped, enabled, same terms_id and profile_hash, and every cap holds after the fill (TokenLifecycleAccounting 8302).
If the budget is enabled, spent + gross <= limit (PremiumBudgetExceeded 6007).
Effects.
quantity_atoms of collateral: customer's account to the position's collateral vault.
gross - fee USDC: your vault to the customer. fee = floor(gross × fee_bps / 10000).
fee USDC, if above 0: your vault to fee_tokens.
Creates the PositionV3 with state = 0 (open) and strike_usdc = strike.
Adds to usage (reserved atoms, reserved strike, premium, fill count), exposure.active and premium_budget.spent.
If your vault holds less than the gross premium, the fill fails with CustodyInvalidAmount (7405) and nothing moves. A nonce that already opened a position fails because the position account exists.
One JitoSOL fill may be worth at most 2 SOL of stake-pool value. The program reads two u64 fields of the Jito stake pool account and requires, in exact integer math:
total_lamports is the u64 at byte 258 of the pool account and pool_token_supply the u64 at byte 266. The pool's last_update_epoch (u64 at byte 274) must equal the current epoch. The largest fill is therefore floor(2,000,000,000 × pool_token_supply / total_lamports) JitoSOL atoms. For example, at the pool rate of 1.306 SOL per JitoSOL on 15 October 2026, that was about 1.53 JitoSOL. The cap is per fill, not per desk or per market. A larger fill fails with JitoSolOrderLimit (6008). Size your quotes from a fresh read of the pool.
Buys the collateral at the strike, inside the exercise window. Ordinary markets only.
Signer
your authority or your operator
SDK
Recommended: exerciseV3Instructions({ market, position, authority, rentLamports, makerCollateral? }) returns the whole transaction. The bare instruction: exerciseV3({ market, position, authority, writerCollateral, writerPayment, makerCollateral }).
Compute
400,000. Set it explicitly: the measured worst case is 277,316, above the default 200,000.
#
Account
w
s
Notes
0
authority
s
Maker.authority, or Maker.operator if set. Otherwise TokenLifecycleAuthority (8304).
1
market
The position's MarketV3.
2
maker
Your Maker.
3
position
w
The PositionV3.
4
collateral_vault
w
The position's collateral vault. Closed by this instruction.
5
writer
Must equal position.writer. Not a signer.
6
collateral
Collateral mint.
7
writer_collateral
w
A collateral token account owned by the writer. Receives any surplus and the vault's rent.
8
collateral program
9
exposure
w
Your Exposure for this mint.
10
usage
w
Your MintUsageV1 for this mint.
11
payment
USDC.
12
payment_vault
w
Your USDC vault. Pays the strike.
13
writer_payment
w
A USDC account owned by the writer. Receives the strike.
14
maker_collateral
w
A collateral token account owned by your authority (also when the operator signs). Receives the collateral.
15
payment program
SPL Token.
No arguments. No remaining accounts.
Checks. The position, market, usage and exposure are canonical and match each other (TokenLifecycleIdentity 8300). The position is open and expiry <= now < expiry + window (InvalidOrder 6003). The vault holds at least strike_usdc, and the collateral vault at least quantity_atoms (CustodyInvalidAmount 7405). writer_collateral, writer_payment and maker_collateral have no delegate and no close authority other than their owner (CustodyUnsafeRecipient 7403).
Effects.
strike_usdc USDC: your vault to writer_payment.
quantity_atoms collateral: position vault to maker_collateral.
Any extra collateral in the position vault, then the vault itself (closed): to the writer.
Subtracts the position from exposure.active and usage. Sets state = 2 (exercised).
The program does not create token accounts, and a writer could leave theirs unusable. exerciseV3Instructions handles this: it returns, in order, a SetComputeUnitLimit of 400,000, then a fresh USDC account and a fresh collateral account for the writer, then exercise_v3 paying into them. The two accounts are created at addresses derived from your signer (createWithSeed), so only your signer can create them and the writer cannot block them. You pay their rent (rentLamports each, read from your RPC), and the writer keeps it. makerCollateral defaults to your authority's associated token account for the collateral, which must already exist. The SDK refuses your operator's associated token account, because the program pays only the authority's. Sign with authority as the only signer and fee payer.
ts
import { createSolanaRpc } from'@solana/kit';
import { WRITER_ACCOUNT_SPACE, exerciseV3Instructions, releaseV3Instructions, typeDecodedMarketV3, typePositionV3 } from'@covrfun/maker-sdk';
declareconstmarket: DecodedMarketV3; // decodeMarketV3 of the position's marketdeclareconstposition: PositionV3; // decodePositionV3, status 'open'declareconstauthority: string; // your authority or operator: signer and fee payerconst rpc = createSolanaRpc('https://<your rpc>');
// Rent for one 165-byte token account. Read it, do not hard-code it: rent has changed before.const rentLamports = await rpc
.getMinimumBalanceForRentExemption(BigInt(WRITER_ACCOUNT_SPACE), { commitment: 'finalized' })
.send();
const ex = awaitexerciseV3Instructions({ market, position, authority, rentLamports });
// ex.instructions: SetComputeUnitLimit(400,000), two fresh writer token accounts, exercise_v3.// Put them in one transaction, in this order, with `authority` as the only signer.const rel = awaitreleaseV3Instructions({ market, position, payer: authority, rentLamports });
// rel.instructions: SetComputeUnitLimit(400,000), one fresh writer collateral account, release.
Returns the collateral to the writer after the exercise window. Anyone can send it. You get nothing back. For a restricted position that was never funded, the instruction is release_restricted_v3, with the same accounts.
Signer
none required. Whoever pays the fee can send it.
SDK
Recommended: releaseV3Instructions({ market, position, payer, rentLamports, collateralAccountSpace? }): a SetComputeUnitLimit of 400,000, a fresh collateral account for the writer created by payer, then the release. payer signs alone. The bare instruction: releaseV3({ market, position, writerCollateral }). Both pick release_restricted_v3 for restricted markets.
Compute
400,000
Accounts: the ten accounts at positions 1 to 10 of exercise_v3, in the same order (market, maker, position, collateral_vault, writer, collateral, writer_collateral, collateral program, exposure, usage). No arguments.
Checks. The position is open and now >= expiry + window (InvalidOrder 6003). The vault holds at least quantity_atoms (CustodyInvalidAmount 7405). writer_collateral must be a clean collateral token account owned by the writer: right mint and owner (Anchor 2014, 2015), no delegate, and no close authority other than the writer (CustodyUnsafeRecipient 7403). The program does not create it: releaseV3Instructions creates a fresh one in the same transaction.
Effects. All collateral in the position vault goes to writer_collateral. The vault is closed and its rent goes to writer_collateral too. Exposure and usage are freed. state = 3 (released). No USDC moves.
release_restricted_v3 returns whatever the vault holds, even if it is less than quantity_atoms. A funded (pending) restricted position can never be released.
Lets the writer close an open position before expiry by paying you a price you signed. The collateral goes back to the writer. No SDK builder or signing helper.
Note
Not offered to desks yet: covr's gateway does not run early-close auctions for V3 positions, so no flow produces this instruction for an external maker today.
#
Account
w
s
Notes
0
writer
w
s
Must equal position.writer. Pays the receipt rent.
Your early-close nonce. Checked against min_nonce. Single use.
amount_usdc
u64
USDC atoms the writer pays you. Above 0.
valid_until
i64
Unix seconds. now < valid_until <= expiry.
signature
[u8; 65]
Your quote key's EIP-712 signature over EarlyClose(bytes32 position,bytes32 owner,address maker,uint64 nonce,uint64 amount,uint64 validUntil,address usd) in the market's quote domain. owner is the writer, usd is USDC_ALIAS.
Effects.amount_usdc from the writer to your vault. All collateral back to the writer. Exposure and usage freed. state = 4 (early closed). Your premium budget is not refunded.
Lets the writer sell an open ordinary position to your desk at a price you signed. Your authority becomes the position's writer: it receives the strike if the original maker exercises, or the collateral at release. No SDK builder or signing helper.
Note
Not offered to desks yet: covr's gateway does not run position-sale auctions for V3 positions, so no flow produces this instruction for an external maker today.
#
Account
w
s
Notes
0
seller
w
s
The current writer.
1
market
2
original_maker
The Maker that sold the call.
3
position
w
4
buyer_maker
Your Maker.
5
buyer_origin
Your MakerOriginV1.
6
recipient
Your authority. Must be a plain wallet with no data.
7
collateral
8
payment
USDC.
9
seller_payment
w
Seller's USDC account.
10
buyer_payment_vault
w
Your USDC vault. Pays amount.
11
buyer_budget
w
Your PremiumBudget.
12
buyer_mint_policy
Your MintPolicyV1 for this mint.
13
purchase_policy
Your MintPurchasePolicyV1 (initialize_mint_purchase_policy_v1).
Your sale nonce. Checked against your min_nonce. Single use.
amount
u64
USDC atoms you pay the seller.
valid_until
i64
now < valid_until <= expiry + window. The sale itself must happen before expiry, on a market that is not paused.
auction_id
[u8; 32]
Not zero.
signature
[u8; 65]
Your quote key's EIP-712 signature over PositionSale(bytes32 auctionId,bytes32 position,bytes32 seller,bytes32 recipient,address maker,uint64 nonce,uint64 amount,uint64 validUntil,address usd) in the market's quote domain.
Effects.amount from your vault to the seller. Adds amount to your premium_budget.spent and purchase usage. Sets position.writer to your authority. The original maker's obligations do not change.
Accounts, in order: authority (w, s), market, maker, position (w), collateral_vault, collateral, collateral program, payment (USDC), payment_vault (w), strike_escrow (w, created, seeds "strike-escrow-v3", position), usage (w), payment program, system program. No arguments.
Checks. Open, unfunded, and expiry <= now < expiry + window (RestrictedTime 8402, RestrictedState 8401). The collateral vault still holds the full quantity (RestrictedImpaired 8404). The vault holds the strike (CustodyInvalidAmount 7405).
Effects.strike_usdc from your vault to the escrow. state = 1 (pending). cancel_eligible_at = now + 604800.
Accounts, in order: caller (s), the ten delivery accounts of release_v3, payment (USDC), strike_escrow (w), writer_payment (w), maker_collateral (w, owned by your authority), maker_authority (w, your authority, receives the escrow rent), payment program. No arguments.
Effects. The full escrow to the writer. quantity_atoms collateral to maker_collateral. Any extra collateral and the vault rent to the writer. state = 2 (exercised). It fails as a whole if any transfer is blocked, for example by a frozen account.
Undoes a funded restricted exercise that could not be delivered. Both parties must sign the same transaction, and only after cancel_eligible_at. No SDK instruction builder. The SDK encodes its argument with encodeCancellationRequestV1.
Accounts, in order: maker_signer (w, s, your authority, never the operator), writer_signer (s), the ten delivery accounts, payment (USDC), strike_escrow (w), maker_payment (w, a USDC account owned by your authority), payment program. The one argument is the 288-byte CancellationRequestV1.
Effects. The full escrow back to maker_payment. The remaining collateral to the writer. The writer keeps the premium. state = 5. A request that does not bind the exact current amounts fails with RestrictedApproval (8403).
The program has one Anchor error enum. Anchor numbers custom errors from 6000. The enum counts up from 0, with fixed jumps (for example CustodyInvalidAccount = 1400), so its codes come in blocks: 6000 to 6014, 7400s, 7600s, 8000s, 8200s, 8300s, 8400s and so on. A unit test in the program pins every code.
A failed transaction shows the code in hex in its last log line, and the name and message in an Anchor log line:
text
Program log: AnchorError thrown in <source file>:<line>. Error Code: InvalidOrder. Error Number: 6003. Error Message: Invalid quote, lifecycle state, nonce or time window.
Program 8ZpbTPnDECPTDGN5VHHKH5gfer6EUTnRHcFwfUAbkJDg failed: custom program error: 0x1773
Codes below 6000 come from Anchor itself, and codes from another program (System, SPL Token) are that program's own. See Errors from Anchor and other programs.
Codes not listed on this page are not returned by the instructions in this reference.
For some errors the program also logs a line that names the exact cause. Look for it just above the AnchorError line.
Log line
Comes with
Reasons you can see
COVR_TOKEN_ACCOUNTING: <reason>
8302
Unbootstrapped (run bootstrap_mint_usage_v1), Paused (mint policy disabled), Terms (policy terms_id or profile_hash differs from the market), Limit (a mint policy cap would be exceeded), Identity, Arithmetic
COVR_TOKEN_CUSTODY: <reason>
7400 to 7406
InsufficientBacking (source account short), Frozen, Uninitialized, Identity (wrong mint or owner), RecipientDelegate, RecipientCloseAuthority, UnsupportedCapability { … }
COVR_RESTRICTED: <reason>
8400 to 8405
Terms, State, Time, Approval, Impaired, Escrow
COVR_MINT_PURCHASE: <reason>
8302 from buy_position_v3
Same reasons as the accounting line, for your purchase policy
Maker premium spend exceeds its enabled lifetime ceiling or overflows
6008
0x1778
JitoSolOrderLimit
JitoSOL order exceeds 2 SOL of stake-pool backing
6009
0x1779
InvalidJitoPool
JitoSOL stake pool identity, accounting state or epoch is invalid
6010
0x177a
MintPolicyIdentity
Mint policy or usage identity does not match the canonical maker/mint/payment cohort
6011
0x177b
MintPolicySchema
Mint policy or usage schema or allocation length is unsupported
6012
0x177c
MintPolicyGeneration
Mint policy generation is zero, stale or exhausted
6013
0x177d
MintPolicyUnauthorized
Only the registered maker owner or configured watcher may pause this policy
6014
0x177e
MintPolicySettings
Mint policy settings are not canonical or omit terms/profile identifiers
Code
When you hit it
What to do
6000
register_maker_v3: the grant was signed for another authority, program or deployment. fill_v3: the quote was signed in another market's domain, or for another writer, auction id, price, size or nonce. Also early_close_v3 and buy_position_v3.
Sign in the domain from registrationGrantDomain() or quoteDomain(decodedMarket). Check your signer against the golden vectors. Signatures must be low-s with v 27 or 28.
6001
configure_maker with a min_nonce below the current one. Vault instructions with a mint that does not have 6 decimals.
Re-read your Maker and send a min_nonce at or above minNonce. Pass the USDC mint.
6002
deposit or withdraw of 0. A fill whose premium or strike is not a whole USDC atom. early_close_v3 or buy_position_v3 with a 0 amount.
Send a positive amount. Price with exactPriceAtOrBelow. Quote sizes that are multiples of minimumExactLot.
6003
fill_v3: the market is paused, now >= expiry, now >= valid_until, valid_until > expiry, the nonce is below min_nonce or is u64::MAX, the size is 0 or above max_quantity_atoms, or the auction id is zero. exercise_v3: outside [expiry, expiry + window), or the position is not open. release_v3: before expiry + window, or not open. Also early close, sale and transfer outside their time rules.
For fills: quote with a nonce at or above min_nonce, and a valid_until inside the market's life. For exercise: send inside the window, with margin for confirmation. Read the position's state first.
6004
buy_position_v3 from a signer that is not the position's writer, or with a recipient that is not a plain wallet. Writer instructions with a recipient or refund account that holds data.
Pass the current writer as seller and your authority's wallet as recipient.
6006
fill_v3: exposure.active + quantity would exceed exposure.limit.
Raise the limit with set_exposure_limit, or wait for open positions to settle.
6007
fill_v3 or buy_position_v3: the budget is enabled and spent + amount would exceed limit. spent never goes down.
Raise limit with set_premium_budget, or disable it.
6008
A JitoSOL fill worth more than 2 SOL of stake-pool value: quantity_atoms × total_lamports > 2,000,000,000 × pool_token_supply.
Quote smaller sizes. The cap is per fill, not per desk. See JitoSOL size cap.
6009
A JitoSOL fill with a different account in the stake-pool slot, with that account writable or signing, or before the pool has been updated for the current epoch. (A missing pool account gives 8800.)
Pass JITO_STAKE_POOL read-only. Near an epoch boundary, retry after the pool updates.
6010
update_mint_policy_v1 with a collateral mint whose token program or decimals changed, or a payment mint without 6 decimals. A fill whose policy or usage does not match your Maker, authority or this mint.
Derive the policy and usage with deriveMintPolicy(maker, mint).
6011
A policy or usage account of the wrong size or version.
Pass the accounts created by initialize_mint_policy_v1.
6012
update_mint_policy_v1 or pause_mint_policy_v1 with an expected_generation that is not current.
Re-read the policy's generation (decodeMintPolicy(...).generation, or the u64 at byte 204) and retry.
6013
pause_mint_policy_v1 signed by a key that is neither your authority nor the watcher.
Sign with the authority or the watcher.
6014
initialize_mint_policy_v1 or update_mint_policy_v1 with a zero terms_id or profile_hash, or a non-zero lifetime limit while it is disabled.
Build settings with mintPolicySettingsFor(decodedMarket, caps).
Every V3 token movement goes through a checked custody step. It runs before the transfer, so a failed check moves nothing.
Code
Hex
Name
Message
7400
0x1ce8
CustodyInvalidAccount
Custody account identity, state or raw layout is invalid
7401
0x1ce9
CustodyUnsupportedCapability
This token capability is not implemented by the custody adapter
7403
0x1ceb
CustodyUnsafeRecipient
Custody recipient has a delegate or foreign close authority
7405
0x1ced
CustodyInvalidAmount
Custody transfer is zero, unbacked or overflows the recipient balance
Code
When you hit it
What to do
7400
A token account in the transfer is frozen.
Use an account that is not frozen. Wrong mint or owner shows up earlier as another code: TokenLifecycleIdentity (8300) for writer_payment and maker_collateral at exercise, and Anchor 2014 or 2015 for writer_collateral and the fill's customer accounts.
7401
A token account carries a Token-2022 extension the program does not support.
Use plain token accounts.
7403
A destination has a delegate, or a close authority other than its owner. At exercise: writer_collateral, writer_payment or maker_collateral. At fill: the customer's USDC account or the fee account.
Use a clean account. At exercise and release, use exerciseV3Instructions / releaseV3Instructions: they create fresh token accounts for the writer in the same transaction.
7405
Your vault holds less than the gross premium (fill) or the strike (exercise, restricted funding). Also: the position's collateral vault holds less than its quantity, or the customer lacks the collateral at fill.
Top up the vault with deposit. For an exercise, do it before the window closes. After the window, anyone can release the position.
#Maker and registration (7600s, 8000s, 8700s, 8800)
Code
Hex
Name
Message
When you hit it and what to do
7600
0x1db0
MakerFenceSchema
Maker fence allocation, discriminator, tail or current data is invalid
The account passed as your Maker is not a well-formed Maker. Pass deriveMaker(makerEthAddress).maker.
7602
0x1db2
MakerFenceUnauthorized
Maker fence requires the canonical account and registered owner authority
The Maker account is not the PDA of its own quote key, or not owned by the program. Pass deriveMaker(makerEthAddress).maker.
8000
0x1f40
MakerOriginSchema
First-registration maker origin schema is unsupported
bootstrap_mint_usage_v1 with a wrong origin account. Pass deriveMaker(makerEthAddress).origin.
8001
0x1f41
MakerOriginIdentity
First-registration origin is not bound to this maker, deployment or payment cohort
Same: the origin belongs to another Maker or deployment.
8002
0x1f42
MakerOriginState
Maker birth fence or unused mint-usage bootstrap state is invalid
bootstrap_mint_usage_v1 on a usage account that is already bootstrapped. It runs once per mint, so check bootstrapped (offset 204) first. Also initialize_mint_purchase_policy_v1 before bootstrap.
8700
0x21fc
MakerMigrationSchema
Maker migration schema or provenance allocation is unsupported
An internal account check failed: the origin account of a fill or sale is not your MakerOriginV1. You should not see this with SDK builders. Pass deriveMaker(makerEthAddress).origin.
8701
0x21fd
MakerMigrationIdentity
Maker migration provenance, historical record or selected seal differs
An internal account check failed: the origin account belongs to another Maker or deployment. You should not see this with SDK builders. Pass deriveMaker(makerEthAddress).origin.
8800
0x2260
MakerMigrationHoldsSchema
Maker migration holds or terminal evidence schema is unsupported
Wrong number of remaining accounts on a fill: a JitoSOL fill without the stake pool account, or any extra account. A JitoSOL fill takes exactly one (JITO_STAKE_POOL, read-only). Other mints take none. SDK builders add it for you.
The market account is not a 376-byte V3 market owned by the program, or its mode does not match the instruction (ordinary versus restricted). Decode it with decodeMarketV3 and use settlementMode.
8201
0x2009
MarketV3Identity
V3 market identity or canonical address is invalid
The market is not at its own PDA, its payment or deployment is wrong, or the collateral mint changed since the market was created (for example a new mint authority). Fills on that market stay closed. Use a newer market.
8202
0x200a
MarketV3Terms
V3 market terms are invalid
create_market_v3 with expiry less than 60 s or more than 90 days away, a window outside 60 to 3,600 s, a zero strike or size, a fee above 1,000 bps, or a zero fee owner.
8203
0x200b
MarketV3Unsupported
Token requires a settlement capability not implemented by ordinary V3
create_market_v3 with native SOL, a mint with a freeze authority, or a Token-2022 mint with extensions.
An account does not match the position or market: wrong exposure, budget, policy, usage, writer, mint or token program; a non-USDC payment mint; an extra remaining account on exercise or release. Rebuild the instruction with the SDK builder from a fresh, finalized read.
8301
0x206d
TokenLifecycleTerms
V3 lifecycle terms or transition are invalid
buy_position_v3 on a position whose terms do not match its market, or a writer instruction on a restricted position.
8302
0x206e
TokenLifecycleAccounting
V3 lifecycle accounting or admission rejected the transition
Your mint policy refused the fill. Read the COVR_TOKEN_ACCOUNTING reason line: Unbootstrapped, Paused, Terms or Limit. Fix the policy with update_mint_policy_v1, or wait for open positions to settle.
8304
0x2070
TokenLifecycleAuthority
V3 lifecycle authority is invalid
exercise_v3 or fund_restricted_exercise_v3 signed by a key that is neither your authority nor your current operator. early_close_v3 signed by someone other than the writer.
Restricted timeout, terms identifier or record mode is not the agreed restricted terms
8401
0x20d1
RestrictedState
Restricted position state does not permit this transition
8402
0x20d2
RestrictedTime
Restricted transition is outside its funding, completion or cancellation time
8403
0x20d3
RestrictedApproval
Restricted cancellation request does not bind both parties to the exact current amounts
8404
0x20d4
RestrictedImpaired
Restricted collateral vault no longer backs the full claimed quantity
8405
0x20d5
RestrictedEscrow
Restricted strike escrow is not exactly funded, fresh or fully refundable
RestrictedTime on fund_restricted_exercise_v3 means you are outside the exercise window. RestrictedImpaired means the position's collateral vault holds less than its quantity, so funding is refused.
System program, on an account the instruction creates
The account already exists. On fill_v3: that quote nonce already opened a position. On setup: you already ran that step.
1
0x1
SPL Token, on deposit or withdraw
Insufficient funds in the source account.
2001
0x7d1
Anchor ConstraintHasOne
The signer is not Maker.authority, or an account does not belong to your Maker.
2006
0x7d6
Anchor ConstraintSeeds
An account is not at the PDA the instruction expects. Re-derive it with the SDK.
2012
0x7dc
Anchor ConstraintAddress
A mint or program account is not the one the market names (for example, not USDC).
2014, 2015
0x7de, 0x7df
Anchor ConstraintTokenMint, ConstraintTokenOwner
A token account has the wrong mint or owner, for example a deposit source not owned by your authority.
3002
0xbba
Anchor AccountDiscriminatorMismatch
An account of the wrong type was passed, for example a Maker where a market is expected.
3007
0xbbf
Anchor AccountOwnedByWrongProgram
An account is owned by another program, for example a Token-2022 mint passed to set_exposure_limit (the SDK's setExposureLimit refuses that before you send).
3012
0xbc4
Anchor AccountNotInitialized
An account the instruction reads does not exist yet. Run the setup step that creates it.
Only the gross premium you bid. covr's fee comes out of that premium before it reaches the customer. You also pay normal Solana fees, and rent for your own accounts: about 0.012 SOL once for registration and setup, at current rent. Each exercise also funds two fresh customer token accounts, about 0.003 SOL.
Send your bid within 3 seconds of the RFQ. If you win, answer before the award window ends, at most 6 seconds after the RFQ. Read the market account once and cache it, so you are not waiting on your RPC for every RFQ.
Yes. Build it with the SDK's withdraw and sign with your authority. It moves USDC from the vault to your authority's own USDC token account at any time. The program does not hold back the strikes you owe, so keep enough in the vault for the exercises you plan.
No. The program checks the vault at each fill and each exercise. Nothing is set aside per position. Your mint policy's reserved-strike cap limits how much strike you can owe at once.
#Can I quote tokenized stocks or create my own markets?
Not yet. Desks quote JitoSOL on covr's markets for now. You can create a market on chain, but customers do not see it yet.
#Why was my bid refused with "verified but not entered"?
You are in practice mode. Your bid passed the signature, terms and asset checks, but covr has not listed your desk on that market yet. See Practice mode for what it does not test.
The program re-checks everything. The gateway verifies signatures and terms, but the program verifies them again at fill. A bad gateway cannot make you pay more than your signed price, for another market, or for a nonce below your floor.
Per-market signing domains. A signature for one market can never be replayed on another.
Your own limits. Exposure, mint policy caps and the premium budget are enforced on chain and set only by your authority.
Brakes you own. Pausing your mint policy stops every fill at once, and a hot watcher key can do it. Raising min_nonce cancels every outstanding quote for good. See Cancel quotes.
Someone can sign quotes in your name, fill them against your vault, sign early-close buybacks of your open positions and, if you created a purchase policy, buy positions with your vault. See If your quote key leaks.
Authority
Someone can withdraw your vault and change your settings.
Gateway token
Someone can read your RFQ feed and take your 16 connection slots, locking your bot out with 4429. They cannot bid or quote: both must be signed by your quote key. Once covr lists your desk, they could set the customer's no-quote note, which needs no signature. Ask covr to revoke it.
Raising min_nonce to your next journal nonce is not enough. Whoever holds the key can sign:
new quotes above that floor, valid until a market's expiry, and fill them against your vault themselves (the 60-second quote limit is the gateway's rule, not the program's);
early-close buybacks of your open positions. The writer of an open position can close it against you for as little as 1 USDC atom, and a writer can hand a position to anyone without your signature. You lose the value of those calls.
position purchases paid from your vault, up to your purchase policy's caps, if you created a purchase policy (buy_position_v3).
Do this, in order:
Pause your mint policy at once. Your authority or your watcher can do it. No new fill on that mint can land while it is paused, and your vault cannot be made to buy a position (buy_position_v3).
Retire the key: set min_nonce to 2^64 − 1 with configureMaker, passing your current operator back. Pausing does not stop early closes; only this does.
Keep the policy paused and, if you like, withdraw the vault to your authority's USDC account (withdraw in the SDK, authority only). Once paused and retired, no quote-key signature can move your USDC. Only your authority (withdraw, exercise) and your operator, if set (exercise), can.
Exercise what you still hold. To exercise the old maker's open positions, keep or deposit their strikes in the old vault before expiry.
Start again with a new quote key: register it as a new maker with the same authority and run setup for it. Ask covr for a new gateway token and to list the new address on its markets.
Before step 1 lands, forged fills and purchases are capped by your vault balance and your limits: the premium budget (shared by both), the mint policy's lifetime premium limit for fills, and the purchase policy's per-order and lifetime caps for purchases. Set them with this in mind. Forged early closes stop only at step 2.