Olive Solana Python SDK
Olive's Long Calls and Long Puts are cash-settled: both pay intrinsic value in USDC, and holders never supply an exercise payment.
This SDK implements the V13 program (devnet 8TjjPPLKhwgmeSRpK9TdZETVk5xcnJCAhoLb7SHnxSr3,
OLIVE_OPTIONS_PROGRAM_ID) and the native /maker/v4/ws protocol (protocolVersion 4.0) and
nothing else. There is no translation or compatibility layer: earlier clients are refused by the
server, and quotes signed for earlier programs cannot be carried forward. Makers moving from 2.x:
see MIGRATION-3.0.md. For pricing, read rfq.leg (request["legs"][0]) and return
rfq.quote(premium=...) or rfq.quote(amount=...).
olive_solana is the maker-facing surface. Keeper, admin and backend tooling (governance and lifecycle
builders, account decoders, entry/exit transaction assembly and instruction encoders, the margin mirror,
Pyth helpers) is olive_solana.operator.
How V13 margining works
- Legs. Entry RFQs, quotes and positions carry
legs:{kind, maturity, strikeE8, qty}(kind0 = call, 1 = put;qtythe unsigned size the user buys). The wire carries 1 to 4 legs; the program accepts exactly one user-long call or put.ManagedMakerdeclines any other structure itself (unsupported_structure) before your code sees it, andrfq.leg(LaunchLeg:kind,kind_name,strike_e8,size,maturity) is the leg to price.assert_launch_structure/launch_legraiseLegErrorwith the program's error name. Maturities sit on the 08:00 UTC grid (is_maturity_on_grid): an off-grid leg failsInvalidMaturityandManagedMakerdeclines its RFQ asinvalid_maturity. - Signed quotes. Entry quotes sign under
OLIVE_ENTRY_QUOTE_V13(the leg, the unsignedpremium); exits underOLIVE_EXIT_QUOTE_V13(the unsignedamount,fractionBpsof the original position and the position'sremainingBpsbefore the exit; no USDC source). You return the quote'spremiumandamount; the SDK signs them. - Margin requirement, not maximum payout. Each bucket holds a price-driven requirement
(
risk.bucket_requirement): every strike slot's time-value add-on plus the worse of the bucket's intrinsic payout at a shocked-up and a shocked-down price. That is netting: calls and puts in the same bucket offset, because only one side of a strangle can pay at expiry. Every fill reprices the whole book at the live price (risk.fill_requirement). - One pool, one balance. Buckets hold no tokens. Every maker has one pool vault, the USDC associated
token account of its
MakerConfig(pool_vault(maker_config, mint)), and the maker ledger splits it intopool_free(the maker's to withdraw) andpool_committed(thecollateralof its buckets). What collateral does not cover is unfunded and is carried by credit (credit_limit, set by governance). You fund by depositing (deposit_pool_instruction; anyone may deposit) and take free money out only withwithdraw_pool_instruction(unrepaid insurance is repaid first;amount=0only repays; refused while defaulted, with a Pending bucket or a matured live bucket). Every fresh reprice (entry, refresh, handover) moves collateral toward the requirement: credit carries what it may (before the deadline, within the limit; an entry keeps at least its bucket's pre-fill unfunded on credit), free pool money covers the rest, and collateral above the requirement goes back topool_free(risk.apply_requirement).maker_balance(capacity)/ManagedMaker.balance()give one maker-level view:pool_free,pool_committed, the requirement, unfunded and credit,insurance_unrepaid,defaultedandneeded_by_next_deadline(what to deposit beforenext_funding_deadline). - Entries. A fill that adds unfunded exposure needs the funding deadline ahead
(
FundingDeadlinePassed) and the maker within its limit afterwards (CreditCapacityExceeded).risk.predict_entry_fundingpredicts the pool pull and both refusals;capacity_entry_fundingandcapacity_max_size(capacity, market_config=..., maturity=..., kind=..., strike_e8=..., max_size=..., now_seconds=...)run it on amaker.capacityresult so you can clip a quote to what will execute (max_size_for_quoteunderneath). Both give nothing for a defaulted, inactive or handed-over maker. - Exits remove quantity from the slot and pay the user from the maker's funding wallet, with no price and
no pool movement: the funding authority's USDC account (
exit_source_address(funding_authority, usdc_mint)) approves the PDAexit_delegate_address(program, maker_config)with an SPLapprove(approve_exit_delegate_instruction, signed by the funding authority, never the quote signer;revoke_exit_delegate_instructionstops every exit), andaccept_exit_quotepays through it. The approved amount falls with every exit and is the cap on what a stolen quote-signer key could move: size it to a day or two of exit flow, not to the wallet.read_exit_approval/check_exit_approvaltell you where it stands (ExitDelegateNotApproved,ExitAllowanceExceeded). The SDK declines (it does not raise) an exit quote aboveledger.exitDelegatedAmount, above the funding wallet's balance (withfunding_wallet_balance=), or while insurance is unrepaid: it sendsrfq.declinewith the reasonexit_approval_too_low,exit_wallet_balance_loworinsurance_unrepaidand reportsexit_declined(amounts, balance, what to do). Passexit_approval=ManagedExitApproval(funding_authority=..., submit=...)toManagedMakerOptionsand the SDK keeps it topped up: when what is left falls below half of the target (25,000 USDC by default;target_usdc,refill_below_bps), or an exit needs more than what is left (then the approval is raised tomax(target, exit)), it builds the SPL approve and awaitssubmit(instruction, current=..., target=...), which must sign with the funding authority and send it, at most once a minute (exit_approval_set/exit_approval_failedstatuses). Without the option it reportsexit_approval_low. - The refill budget. The auto top-up refills the approval each time it drops below half. On its own that would
turn the approval into no limit at all: an attacker who signs exits with a stolen quote-signer key makes the bot
refill the allowance after every one. So the SDK adds at most
max_refill_usdc_per_24hto the approval in any rolling 24 hours (default: twice the target, 50,000 USDC for the default target; the first approval at start-up counts). When the budget is used it stops refilling and reportsexit_approval_budget_exhausted(default log at error level, with the time the window reopens). Exits above what is left are then declined until the window moves on or you approve by hand. The trade-off is explicit: a busy legitimate day can use the budget and decline exits; raisemax_refill_usdc_per_24hif your real flow needs it, knowing that it is also what a stolen key can move per day. The budget lives in memory, so a restart opens a fresh window. - Exits you did not quote. Every
exit.filledis compared with the exit quotes this process signed. A fill against a quote it did not sign raises theunquoted_exitstatus and anEXIT YOU DID NOT QUOTEline at error level (warn level when the quote was issued before this process started, since an earlier run may have signed it). With one bot per MakerConfig that means your quote-signer key signed elsewhere: revoke the exit approval first, then rotate the key. If several processes quote with the same signer, setalarm_on_unquoted_exits=False. - The money log. Every event that moves or endangers your money is logged by default, whatever handlers you wired,
unless
quiet=True: funding needed, settled short (default), true-up paid or partly paid, default cleared, insurance drawn, credit breached, pool deposited and withdrawn (a withdrawal is a WARN: it used the maker-authority key), routing paused and resumed, and every exit-approval status. A line carries amounts in USDC, the transaction signature and the new balance where known, for exampleFUNDING NEEDED: deposit at least 12,345.67 USDC into your pool within 3h 20m (deadline 2026-10-17 08:00 UTC, bucket 7Hk2...4fQa). Lines go to stderr; passlogger=to send them elsewhere. - Nothing due. Entries are refused while the maker has a Pending bucket (
PendingBucketsOutstanding; the free pool is then held for the true-up and reprices pull nothing from it) or a live bucket past its maturity (EarlierMaturityOutstanding);capacity_max_sizereturns 0 then. A settlement that leaves a bucket Pending puts the maker in default in that call (bucket.settled.makerDefaulted, true only on that call). The default clears by itself (maker.default_cleared,on_maker_default_cleared) once nothing is due, unless insurance was drawn, a bucket was Haircut or one was handed over (ledger.defaultCanCure,default_can_cure(episode)); there are no incidents; governance reactivates a maker only once none of its buckets is live (LiveBucketsOutstanding). - One bucket per expiry. A maker has exactly one bucket per market and maturity, with 256 strike
slots, at
bucket_address(program, maker, market, maturity)(no index). Its address derives from the maker that created it even after a governance handover (originMakervsmakerConfigon bucket objects;bucket.handed_over). - 24h funding deadline and the freeze. Credit stops carrying a bucket 24 hours before maturity
(
funding_deadline(maturity)). At the deadline the keeper reprices every live bucket and free pool money covers what it can. A maturity still unfunded at or past its deadline freezes the maker (is_underfunded_past_deadline,ledger.underfundedPastDeadline,ledger.maturities[]): entries on every maturity, withdrawals (repay-only too) and handovers to it are refused (UnderfundedPastDeadline) until a refresh covers it. Nothing defaults at that point; the maker defaults only if the bucket then settles short. - Repeated warnings and the routing gate. Olive sends
bucket.funding_requiredat the deadline and 12 h, 6 h, 3 h, 2 h and 90 min before expiry while the bucket is short, plus one extra notice when the deficit has grown since the last one by at least the larger of 5% of the requirement and 100 USDC, one per deposit that leaves the bucket short (with what is left), and one when the routing pause begins; each carries the current deficit andsecondsUntilDeadline(negative after the deadline). A maker receives no new entry requests while a maturity is short past its deadline (funding_shortfall, at once: the program refuses its entries anyway), a bucket is Pending with a gap (settlement_shortfall) or the maker is deactivated on chain (maker_inactive).maker.capacityreportsrouting: {paused, reason?, since?}on both scopes (reasonandsinceabsent, not null, when not paused) andmaker.routing_paused/maker.routing_resumed(on_routing_paused/on_routing_resumed) announce it. Exits, funding, true-up and settlement continue. These events are durable but are not money-at-risk and need no handler.ManagedMakerreportsfunding_warning,routing_pausedandrouting_resumedthroughon_statuswithout any handler, and does not fund anything itself: react toon_funding_requiredandon_routing_pausedby building adeposit_pool_instructionfor the notice'sdeficit(orneeded_by_next_deadline), signing it with the funding authority and sending it. - 4h true-up. A bucket that settles short enters
pendingwith a 4-hour true-up window (bucket.true_up_pending,true_up_deadline; anyone mayfund_bucket). Afterwards the pool, then insurance (insurance.drawn) pay, and holders are paid pro rata (haircut); the maker defaults. - Headroom.
maker.capacity(scopeall) returns the makerledger(poolVault,poolFree,poolCommitted,creditLimit,unfundedExposure,availableCredit,breached,defaulted,insuranceDrawn,insuranceUnrepaid,pendingBucketCount,exitSource,exitDelegatedAmount,maturities[]andunderfundedPastDeadline), the margin constants, each market's risk parameters,minLegNotionaland live price, and the maker's one bucket per expiry (required,collateral,unfunded,payoutPool,payoutRemaining,live, deadlines and occupied slots{index, kind, strikeE8, qtyShort}). Scopeledgerreturns only the ledger and routing.credit.breachedreports unfunded exposure above the limit after a price move;credit.limit_changeda new limit.
Every V13 instruction has a builder, and each derives its PDAs, vaults and Pyth push account so you pass
only what cannot be derived (a bucket by bucket= or maturity=, a live price by price_update= or the
market's oracle_feed_id=): olive_solana.lifecycle (initialize, fund, refresh, handover,
record_settlement with an optional FallbackPrice, settle, redeem, sweep_position, claim_swept_payout,
close), olive_solana.governance (protocol, roles, makers, set_credit_limit, markets, insurance),
olive_solana.pool (deposit_pool, withdraw_pool, maker_balance) and olive_solana.transactions
(entry, exit and transfer, quote_signature_instruction, build_quote_transaction for a v0 transaction
with an address lookup table, entry_lookup_table_addresses). olive_solana.accounts decodes the 8
program accounts. Entries and exits are v0-only: build_quote_transaction requires the market's lookup
table (section 7 of the interface: the user's USDC source and the position stay static; a one-leg entry is
682 bytes). decode_program_error also explains Anchor's 2040 ConstraintDuplicateMutableAccount (a self-handover or a
claim into the claims vault) in its message. olive_solana.pyth normalizes Pyth exponents -18..0 to -8 and checks a settlement update
(validate_pyth_benchmark_message). Sign and send with your own custody.
ManagedMaker is the recommended API. Maker code supplies the entry premium or the
exit amount; the SDK validates requests, binds canonical quote
fields, allocates quote IDs and entry nonces, signs through a local/HSM callback,
reconnects, rebuilds the bot's view from Olive on every connect (on_sync) and
routes lifecycle events. It keeps no database: since 2.0 Olive is the source of
truth (see "State without a database").
Install
# From this repository:
pip install -e "packages/python-sdk"
# After the distribution is published:
pip install "olive-solana-sdk"
Python 3.11 or newer is required. The SDK needs no database.
Managed maker quickstart
import asyncio
import os
import time
from olive_solana import (
DeploymentIdentity,
LocalKeypairMakerSigner,
ManagedMaker,
ManagedMakerEventHandlers,
ManagedMakerOptions,
OliveSnapshot,
capacity_max_size,
entry_strike_grid_warning,
snapshot_covers,
)
# The bot's book: Olive's positions, keyed by position address.
book: dict[str, dict] = {}
async def main() -> None:
signer = LocalKeypairMakerSigner.from_json_file(
os.environ["OLIVE_QUOTE_SIGNER_KEYPAIR"]
)
async def sync(snapshot: OliveSnapshot) -> None:
# Start and every reconnect: replace the book with Olive's, then
# reconcile hedges to it. Nothing is dispatched before this returns.
book.clear()
book.update(snapshot.positions)
await rehedge(book)
async def filled(event) -> None:
if snapshot_covers(maker.snapshot, event):
return # already in the snapshot
# State-driven: re-read the position and set it; a redelivery changes nothing.
book[event["position"]] = (await maker.position(event["position"]))["position"]
await rehedge(book)
async def exited(event) -> None:
book.pop(event["position"], None)
await rehedge(book)
async def entry(rfq):
request = rfq.request
leg = rfq.leg # legs[0]: the one user-long call or put of a launch RFQ
# Olive only sends on-grid strikes; this is a cheap defensive check.
if entry_strike_grid_warning(request) is not None:
return rfq.decline("strike_off_grid")
capacity = await maker.client.capacity("all")
headroom = capacity_max_size(
capacity,
market_config=request["marketConfig"],
maturity=leg.maturity,
kind=leg.kind,
strike_e8=leg.strike_e8,
max_size=leg.size,
now_seconds=int(time.time()),
)
if headroom < leg.size:
# RFQs are all-or-nothing: the bucket cannot back the full size.
return rfq.decline("insufficient_credit")
premium = await price_entry(leg, request)
return (
rfq.decline("outside_risk_limits")
if premium is None
else rfq.quote(premium=premium)
)
async def exit_(rfq):
amount = await price_exit(rfq.request)
return (
rfq.decline("outside_risk_limits")
if amount is None
# Paid from your pool's free money; the SDK refuses what the pool ledger cannot pay.
else rfq.quote(amount=amount)
)
maker = ManagedMaker(
ManagedMakerOptions(
url=os.environ["OLIVE_MAKER_WS_URL"],
maker_config=os.environ["OLIVE_MAKER_CONFIG"],
expected_deployment=DeploymentIdentity(
program_id=os.environ["OLIVE_PROGRAM_ID"],
genesis_hash=os.environ["OLIVE_GENESIS_HASH"],
chain_tag=os.environ["OLIVE_CHAIN_TAG"],
),
signer=signer,
on_sync=sync,
assets=("btc", "sol"), # the topics this session owns
topics=("btc", "sol"),
products=(0, 1),
on_entry_rfq=entry,
on_exit_rfq=exit_,
events=ManagedMakerEventHandlers(
# Selection is feedback, not execution.
on_entry_selected=record_selection,
# Only finalized fills change the book and the hedge.
on_entry_filled=filled,
on_exit_filled=exited,
on_position_redeemed=exited,
on_position_expired=exited,
# Money-at-risk margin events need a handler (or on_event).
on_funding_required=deposit_into_pool,
on_true_up_pending=true_up_before_deadline,
on_credit_breached=alert_credit_breach,
on_settlement=record_settlement,
),
)
)
await maker.run()
asyncio.run(main())
capacity_max_size answers for the whole size: before the 24h deadline it
counts your credit room and then your free pool money; inside it, free pool money only.
The quickstart leaves price_entry, price_exit, rehedge (move the hedge to
the target the book implies) and the other event handlers to you.
Olive's onboarding test request
While a maker is onboarding, Olive sends each chosen asset one test request (an rfq.request with test: true) the first time
the software subscribes, so the first-quote check completes in seconds. It is never an auction and can never be filled.
ManagedMaker answers it itself and never calls on_entry_rfq for it: by default a signed quote at the five-USDC minimum
premium, or whatever on_test_rfq(rfq) returns (rfq.decline(reason) means "connected, not quoting yet" and does not complete
the first quote). It asks for it with the x-olive-features: test-rfq header; a plain MakerWsClient is sent none unless you
set accept_test_requests=True. load_olive_env() reads the olive.env the onboarding page gives you (a variable already set wins).
State without a database
Olive already keeps everything a maker would otherwise persist: it refuses a
reused nonce or quote id per maker, settles each fill exactly once on chain,
and keeps the durable event log (event.ack, event.recover) server-side. So
the SDK keeps no state across restarts, and a bot rebuilds from Olive:
on_sync(snapshot)(required) runs on start and after every reconnect, before any lifecycle event or RFQ of that connection reaches maker code; events arriving meanwhile are held. The SDK subscribes first, then pagesmaker.positions(active) to the end and readsmaker.capacity.OliveSnapshothasslot,positions(a mapping keyed by position address),capacity,connection_idandsequence. A sync that fails, or anon_syncthat raises, is retried with backoff (sync_failedstatus); nothing is dispatched until one succeeds. RFQs wait for the sync until their auction ends (rfq_skippedotherwise).snapshot.slotis a lower bound: every finalized change at or below it is in the snapshot (later ones may be too).snapshot_covers(maker.snapshot, event)is true for a finalized chain event (it hastransactionSignature) at or below that slot: skip it. Anything else must be applied.- Delivery is at least once. An event is acknowledged after its handler
returns; a handler that raises is retried and the event stays
unacknowledged, and Olive redelivers what it did not see acknowledged (after
a crash, a reconnect or
event.recover). Write handlers state-driven and idempotent: key the book by position or bucket, set state rather than add to it, and make external side effects (hedges, transfers) target-based rather than incremental. A bounded memory of recently handled event ids acknowledges a redelivery within one process without dispatching it again. maker.position(address)(andclient.position(address)) reads one of the maker's positions, in any status, through themaker.positionmethod.- Quote ids are random; nonces count up from a random 63-bit start per process and quote kind. A signed quote is remembered in memory, bounded and forgotten once its RFQ is final or expired, so the same RFQ offered again after a reconnect gets the exact same signed quote. After a restart, in-flight quotes are simply dropped: they expire.
- Every in-process map is bounded (
MANAGED_MAKER_LIMITS, overridable withManagedMakerOptions.limits); the event buffer holdsevent_queue_capacityevents and leaves the rest unacknowledged for redelivery (event_dropped).
Funding and true-up events (on_funding_required,
on_true_up_pending) must have explicit handlers unless the generic on_event
handler covers them; otherwise the event is never acknowledged, and Olive
refuses the maker new entries while one is unacknowledged. A catch-all on_event still
satisfies that gate (kept for 3.x), but it is not how you find out: the SDK logs both events
itself with their amounts before any handler runs, and Olive's skeleton wires both handlers
explicitly. Wire real handlers (deposit, page a human). The other margin
events (bucket.refreshed, bucket.funded, bucket.settled, pool.deposited,
pool.withdrawn, insurance.drawn, credit.breached, credit.limit_changed) have optional handlers.
Two off-chain events report the routing gate:
| Event | Handler | Gates entries | Meaning |
|---|---|---|---|
maker.routing_paused |
on_routing_paused |
no (the pause itself stops new entry requests) | A maturity is short past its funding deadline (reason "funding_shortfall"), a bucket is Pending with a gap ("settlement_shortfall") or the maker is deactivated on chain ("maker_inactive"); an open set, treat an unknown reason as paused, with bucket, marketConfig, maturity, deficit and pausedAt. Sent again if the reason changes. Fund it now (your code does that). |
maker.routing_resumed |
on_routing_resumed |
no | None of the buckets qualifies any more: covered or settled Solvent (resumedAt). Never sent for a pause whose paused notification you did not receive. |
Their typed payloads are RoutingPaused and RoutingResumed.
Five finalized chain events report hand-overs and parameter changes:
| Event | Handler | Gates entries | Meaning |
|---|---|---|---|
bucket.handed_over |
on_bucket_handed_over |
no | Governance handed a defaulted maker's Unsettled bucket to a substitute (previousMaker, newMaker, priceE8, releasedToPrevious); sent to both makers. |
credit.limit_changed |
on_credit_limit_changed |
no | Governance set the maker's creditLimit; refresh cached credit headroom. |
maker.status_changed |
on_maker_status_changed |
no | The maker's on-chain active flag changed (clearedDefault when a reactivation cleared a default). |
market.updated |
on_market_updated |
no | A market's controls, risk parameters and minLegNotional were replaced; refresh cached maker.capacity. |
All four carry transactionSignature, slot and occurredAtMs; bucket.handed_over
also carries the bucket state fields. Their typed payloads are
BucketHandedOver, CreditLimitChanged, MakerStatusChanged and MarketUpdated.
The local keypair in this quickstart is for laptop tests or a tightly controlled
key-file deployment. Production remote/HSM integrations use
CallbackMakerSigner and never pass private-key bytes to the maker process:
from olive_solana import CallbackMakerSigner, SignerContext
async def sign_with_remote_provider(
digest: bytes,
context: SignerContext,
) -> bytes:
if len(digest) != 32:
raise ValueError("expected a 32-byte Olive digest")
signature = await remote_provider.sign_ed25519(
key_id="olive-quote-signer",
message=digest,
audit_context={"purpose": context.purpose},
)
if len(signature) != 64:
raise ValueError("expected a 64-byte Ed25519 signature")
return signature
signer = CallbackMakerSigner(
public_key="<remote Ed25519 public key in canonical base58>",
callback=sign_with_remote_provider,
)
The callback signs the supplied raw 32-byte SHA-256(preimage) digest with
ordinary Ed25519 and returns the raw 64-byte signature. Do not hash again, sign
encoded text/the full preimage, or use Ed25519ph. The remote public key may be
the finalized on-chain MakerConfig.quoteSigner.
An empty topic or product collection subscribes to everything authorized by
maker.hello. Keep takeover_existing_session=False for ordinary starts; enable
it only for an intentional failover.
After a silent network drop the gateway may still hold your session. Since 3.0 the
reconnect names the lost connection (previousConnectionId in auth.respond) and
the gateway replaces that session at once; a second instance of your maker cannot
name it and is still refused. If a reconnect is refused anyway, a client that had a
session of its own reports session_release_wait through on_status and retries,
at most about 2 seconds apart, for the lease plus 30 seconds from the moment it lost
the connection; refusals past that, or on a first connect, still raise.
ManagedMaker does not retry a sync while disconnected: sync_failed is a failure
on a live connection.
One session per asset
Since 1.3.0 every session owns an explicit set of assets: assets (1-32 unique
lowercase topics) is required on ManagedMakerOptions, MakerWsClientOptions and
MakerDoctorOptions (--assets / OLIVE_ASSETS on the CLI), and topics must be a
subset. List every asset to quote them all in one session. There is no all-assets
mode. The gateway fixes the claim for the session's whole life: maker.hello.assets
lists it, and the client fails terminally if it differs from the request.
- Run one process per asset if you like; each session syncs and acknowledges its own assets' events (maker-wide events go to every session).
SESSION_ACTIVEnames the sessions in the way inconflicting_assetson theMakerWsRpcFailure(the terminal error's__cause__) and in thesession_release_waitstatus.takeover_existing_session=Truereplaces every overlapping session (close 4001), only for the assets you claim. The restart wait of 1.2.1 works per asset session.- Credit, funding and the routing pause stay maker-wide:
maker.capacityshowsledger.reservedCreditacross all sessions, while its other rows andmaker.positionsare filtered to the session's assets. - Stagger process starts by about one second.
FORBIDDENwithsession_limitreportsmaxSessionsPerMaker;asset_not_claimedmeans the session does not own the topic it used.
Onboard from a server
If your signing key lives on a server, one command takes a maker from Olive's invitation to a passing readiness
check. Ask Olive's console for a server token (olv_onb_...), then:
export OLIVE_ONBOARDING_TOKEN=olv_onb_... # or --token; the env var keeps it out of shell history
olive-maker-onboard --keypair ./maker.json --assets btc,sol
It proves the key is yours (it signs the plain-text ownership message Olive writes, with --keypair as the maker
authority; nothing is a transaction), waits for Olive's approval (Olive registers the maker with a temporary maker
authority and proposes yours; on devnet it also sends 0.05 SOL to your maker authority and funding authority for fees),
then, in the page's Activate stage, takes the account over (accept_maker_authority), deposits into your maker pool
(deposit_pool, signed by the funding authority) and approves exits (below): with one key that is one transaction, in
that order (the take-over first, so nothing can be deposited while Olive's temporary key still owns the account;
--two-step signs the take-over alone first, and separate keys always do). Olive sends devnet test funds when it approves
the maker, so there is no faucet step. There is no default deposit amount.
Without --pool-deposit <usdc> the command shows a suggestion with its reason (keep the smaller of 25,000 USDC and a tenth of
the wallet for exits, deposit the rest; on a wallet of 100,000 USDC or more you must type the amount, below that Enter
accepts the suggestion), and --yes refuses to run without --pool-deposit. The prompt names where the money can come back to
(only your maker authority wallet, and not while you are in default, owe a settlement or are short at a funding deadline).
--pool-deposit only fills an empty pool: a rerun of the same command leaves a funded pool alone (it also reads the pool vault
on chain, so a deposit Olive's page has not caught up with yet is seen), and --add-to-pool lets it add, after a prompt that
shows the current balance and the new total. It approves the exit delegate on the funding authority's USDC account with an SPL
approve (what exits may pay from the funding wallet, not from the pool; default 25,000 USDC or the wallet's balance when
lower, or --exit-approval <usdc>; an amount above the wallet's USDC is warned about), and writes olive.env (0600, in --out, default the current directory) and the starter maker olive_maker.py (never over an existing
file). Start it (python olive_maker.py; it reads olive.env itself and answers Olive's test request) and the page shows each
asset connected, its first quote, then Ready. The readiness check is optional: --doctor runs it and sends its report to Olive.
The command never deposits before the takeover (it prints "Take over your maker account first"): until then Olive's temporary key could withdraw the pool.
Rerun it at any time: it reads the current state and continues. Before signing anything it checks the message text binds
exactly your values (environment, program, maker config and id, the four roles, assets, not expired), and it checks every
transaction semantically: exactly one deposit_pool of the amount from your funding authority's USDC account into your
maker's pool, or Olive's accept_maker_authority for your maker,
the signing key the only signer and fee payer (the exit approval is built by the command itself, not taken from the server). Compute
Budget instructions and a different blockhash are fine; anything else is refused and named. Other flags: --api-url,
--products 0,1, --yes (skip the confirmation prompts, not the checks; it needs --pool-deposit), --two-step, --doctor, --add-to-pool, --rpc-url,
--wait-timeout <seconds>, --json (one JSON object per line).
By default one key does everything. Two options change where money and quoting authority sit:
--premium-receiver-owner <wallet>pays premiums to that wallet's USDC account (any address, including an off-curve vault or multisig address). Olive creates the account when it registers the maker, the wallet never signs, and the message you sign names that wallet. Left out, premiums go to the funding authority.- Separate keys:
--keypairstays the maker authority and signs the registration. Name the others with--quote-signer <pubkey>(the hot key on the quoting server) and--funding-authority <pubkey>(the custody wallet that holds the funds); the command checks the message names exactly those (with--quote-signer-keypair/--funding-keypairit also adds their signatures as extra proof). It cannot sign for keys it does not hold, so in separate mode:- the pool deposit is skipped with a pointer to the console's "Sign with another tool" panel, unless you pass
--funding-keypair <path>(it must match the funding authority; refused otherwise); the exit approval, also signed by the funding authority, waits for it; - the readiness check runs only with
--quote-signer-keypair <path>(it must match the quote signer); otherwise the command prints what to run on the quoting server; olive.envnames--quote-signer-keypairasOLIVE_QUOTE_SIGNER_KEYPAIRwhen given; otherwise it keeps Olive's placeholder, which you point at the hot key file. It never names the maker authority key in separate mode;- the test funds Olive sends at approval go to the funding authority, so they arrive without its key.
- the pool deposit is skipped with a pointer to the console's "Sign with another tool" panel, unless you pass
olive-maker-onboard --keypair ./authority.json --assets btc,sol \
--quote-signer <HOT_PUBKEY> --funding-authority <VAULT_PUBKEY> --premium-receiver-owner <VAULT_PUBKEY>
Rerunning a separate-key maker needs the same --quote-signer / --funding-authority flags.
Operational preflight
The doctor authenticates without subscribing, taking over a live maker, or submitting quotes. Its convenience CLI intentionally uses a local test keypair:
olive-maker-doctor --topics btc,sol --products 0,1
olive-maker-doctor --json > maker-doctor.json
Connection and deployment values default from OLIVE_MAKER_WS_URL,
OLIVE_MAKER_CONFIG, OLIVE_PROGRAM_ID, OLIVE_GENESIS_HASH,
OLIVE_CHAIN_TAG, and OLIVE_QUOTE_SIGNER_KEYPAIR. --exit-approval-target <usdc> (default 25000) is the approval you
intend to keep, and --rpc-url lets the doctor read the funding wallet's USDC.
By default the doctor reads maker.capacity with scope all and reports
credit (limit, unfunded exposure and available credit; fail when breached,
warning with no credit), pool (fail while defaulted, warning with what to
deposit before the next funding deadline), insurance (fail while insurance
draws are unrepaid), bucket_margin (requirement, collateral and unfunded per
open bucket; fail when a bucket is unfunded past its funding deadline),
routing (fail while Olive has paused new entry requests) and true_up (fail
while a settled bucket owes a true-up). The same checks are available as
margin_checks(capacity, now_seconds). capacity_state fails while the maker is frozen
(underfundedPastDeadline) and exit_approval reports what the funding wallet has approved to the exit
delegate (fail at 0 and below half of the target, warning below the target). exit_wallet compares the funding
wallet's USDC with the approval (fail below the approval, warning below the target or when not checked). Pass --exit-only
(exit_only=True) to read only the ledger scope.
Remote/HSM deployments MUST NOT export their private key to use the CLI. Call
run_maker_doctor(MakerDoctorOptions(..., signer=signer, assets=["btc"])) programmatically with
the same CallbackMakerSigner used by the managed maker, and retain
report.to_dict() as the machine-readable preflight record.
Strike grid
Entry strikes sit on a fixed grid of round levels per market. The step depends
on how far the strike is from the request's reference price (referencePriceE8):
| Market | within 5% | 5% to 15% | beyond 15% |
|---|---|---|---|
| BTC | $1,000 | $2,000 | $5,000 |
| SOL | $1 | $2 | $5 |
| GOLD / PAXG | $25 | $50 | $100 |
| SP500 / SPYX | $5 | $10 | $10 (one tier beyond 5%) |
- Symbols are case-insensitive, and both the app names (GOLD, SP500) and the on-chain names (PAXG, SPYX) work.
- Re-striking. If the price falls so far that the finest step is wider than 5% of spot, every step is divided by ten until it fits: SOL at $15 strikes on $0.10 / $0.20 / $0.50, BTC at $15,000 on $100 / $200 / $500.
- Boundary tolerance. Near a tier boundary the API accepts a strike that is
a multiple of either neighbouring tier's step, within 1 percentage point
(
STRIKE_GRID_SLACK_PCT). With BTC at $98,000, $103,000 is 5.1% out, where the step is $2,000, but it is accepted because it is within 1 point of the $1,000 tier. - A market without a configured grid gets one tier with a 1-2-5 step near 0.1% of spot.
The API rejects an off-grid entry RFQ (INVALID_STRIKE, reason
strike_off_grid), so a maker should never receive one. The SDK lets you
check anyway, with exact decimal arithmetic (no binary floats):
from olive_solana import (
STRIKE_GRID_SLACK_PCT, entry_strike_grid_warning, grid_levels, grid_step,
nearest_strike, on_grid, strike_grid_for,
)
grid = strike_grid_for("BTC", "112480") # tuple of StrikeTier(within_pct, step)
on_grid(grid, "112480", "113000", STRIKE_GRID_SLACK_PCT) # True
grid_step(grid, "112480", "130000") # Decimal('5000')
nearest_strike(grid, 0, "112480") # Decimal('113000'), call side
nearest_strike(grid, 1, "112480") # Decimal('112000'), put side
grid_levels(grid, "112480", "100000", "125000") # every level in the range
entry_strike_grid_warning(rfq.request) # None when on grid, else a message
Prices can be Decimal, int, str, Fraction or (converted through their
decimal text) float. entry_strike_grid_warning reads underlyingSymbol,
legs[*].strikeE8 and referencePriceE8 and judges each strike exactly as the API does.
The grid matches the TypeScript SDK (the one the app and the API use); the repository's
packages/solana-sdk/test/vectors/strike-grid.golden.json holds the shared test vectors.
Conformance runs can pass the entry requests they received as
run_maker_conformance_suite(..., observed_entry_requests=[...]). Any off-grid
request is listed as a [WARN] line under entry_quoting; it does not fail
the report, because it points at a gateway or fixture problem, not at the
maker.
Wire types and schema
The complete required/optional DTO catalog is published at
maker-websocket.schema.json
and included in the wheel at
olive_solana/schemas/maker-websocket.schema.json. Python exports typed RFQ,
quote, selected, filled, decline, and error/result shapes from olive_solana.
Missing required fields fail before maker callbacks run; unknown additive server
fields are preserved, while outbound quotes remain strict. The normative
semantics and signing preimages are in the
maker WebSocket specification (protocol 4.0).
Reliability contract
- Wire integers remain canonical decimal strings.
- Olive refuses a reused entry nonce or quote id per maker; exit quotes have no wire
nonce and are replay-safe because each binds the position's
remainingBps, which the exit changes. - A reconnect resends only the exact signed quote the process already submitted.
- Durable events are acknowledged after their handler returns; a lifecycle event
carrying one of this maker's quotes must match the quote this process submitted
or verify against the quote signer, otherwise it is neither dispatched nor
acknowledged (
event_rejected). quote.selectedandexit_quote.selectedare not fills. Only finalizedtrade.filledandexit.filledare canonical execution signals.- Signatures and private signer data are never included in SDK status events.
The low-level MakerWsClient remains available for makers requiring custom
orchestration. Releases must pass the shared fixture/digest suite and all
fail-closed maker conformance scenarios.
Development
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/ruff check .
.venv/bin/mypy src
.venv/bin/pytest
.venv/bin/python -m build
Release
PyPI releases use trusted publishing from
.github/workflows/publish-python-sdk.yml; no long-lived API token is stored in
GitHub. Configure the PyPI publisher for repository macols77/olive, workflow
publish-python-sdk.yml, and environment pypi, and protect the pypi
environment with required reviewers.
Pushing a python-sdk-v<pyproject version> tag only builds and verifies; it
never uploads. The build job:
- requires the tag's commit to be on
origin/main(merge first, then tag the merged commit); - when a
coordinated_tag(solana-sdk-v*) is given, requires it to point at the same commit, and otherwise warns with the TypeScript tags found on that commit; - requires the SDK's program id to equal the program's
declare_id!and to be live (executable) on devnet; - requires the tag to equal
python-sdk-v<pyproject version>; - runs ruff, mypy and pytest, builds and
twine checks the wheel and sdist, and reruns the tests against the built wheel in a clean environment.
To publish, run the workflow manually (Run workflow) with release_tag set
to the existing tag, publish typed as PUBLISH, and coordinated_tag set to
the sibling TypeScript tag for a coordinated release. The publish job then waits
for the pypi environment's reviewers and uploads both distributions. The same
dispatch retries a failed release.
Metadata
Release files for olive-solana-sdk 3.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| olive_solana_sdk-3.1.0.tar.gz | 248.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| olive_solana_sdk-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 417.9 kB
Release files / olive_solana_sdk-3.1.0.tar.gz
| Download URL | olive_solana_sdk-3.1.0.tar.gz |
|---|---|
| Size | 248.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ed5eff3011a0990cf2ae271a531aede11a52e28093db224cb37e5a9e4ed3bce
|
|
BLAKE2b-256 checksum How to use checksums |
9f40fdce8b59c6bb3c2dfef7a155a6ac9a2afbbe34bc17994681efeb0601d898
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / olive_solana_sdk-3.1.0-py3-none-any.whl
| Download URL | olive_solana_sdk-3.1.0-py3-none-any.whl |
|---|---|
| Size | 169.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d572e9c557ef00a0c7b8984513b2bd3899fe87c91c36335db88c21b5687920d
|
|
BLAKE2b-256 checksum How to use checksums |
a23504cdd06c96a909596d0667bffc833db39992d0774a590bcdf21177a1b2c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log