Skip to main content

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.

To write a bot, read Write a bot: four functions and one setting. What can go wrong and who handles it: Risks and who handles them.

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.

Write a bot

A maker bot is four functions. It moves no money. ManagedMaker does everything else: it connects, checks and signs your quotes, reconnects, rebuilds its view from Olive on every connect (no database), and turns every money or operations situation into an alert.

You write What it does
on_entry_rfq Price a sale: a user buys a call or put from you. Return rfq.quote(premium=...) or rfq.decline(reason).
on_exit_rfq Price a buyback: a holder sells back early. Return rfq.quote(amount=...) or rfq.decline(reason).
on_fill Hedge. One function, given to both on_entry_filled and on_exit_filled.
on_alert Page a human. Every money or operations situation arrives here with a stable code, and data carries the amount and the deadline.

Olive checks your margin before it sends you a request, again when you quote and when your quote is selected, and the program checks it at the fill, so the bot needs no margin code. What can go wrong and who handles it: Risks and who handles them.

import asyncio

from olive_solana import MakerAlert, ManagedFundingOptions, ManagedMaker, ManagedMakerEventHandlers


def on_entry_rfq(rfq):
    """Price a sale. rfq.leg is the call or put the user buys (kind, strike_e8, size, maturity)."""
    premium = my_premium(rfq.leg, rfq.request)  # USDC base units for rfq.leg.size, or None
    return rfq.quote(premium=premium) if premium else rfq.decline("outside_risk_limits")


def on_exit_rfq(rfq):
    """Price a buyback of rfq.request["fractionBps"] of the position. It is paid from your funding wallet."""
    amount = my_buyback(rfq.leg, rfq.request)
    return rfq.quote(amount=amount) if amount else rfq.decline("outside_risk_limits")


async def on_fill(event) -> None:
    """A sale (trade.filled) or a buyback (exit.filled), final on chain. Olive may deliver an event twice: hedge to
    the target your book implies, not by adding the event's size."""
    await my_hedger.rebalance(event["position"])


def on_alert(alert: MakerAlert) -> None:
    """alert.code is stable (funding_due, true_up_pending, buyback_allowance_low, ...); alert.message is plain English."""
    my_pager.send(alert.severity, alert.code, alert.message, alert.action, alert.data)


async def main() -> None:
    maker = ManagedMaker.from_env(  # reads olive.env and the bot key file (OLIVE_QUOTE_SIGNER_KEYPAIR)
        on_entry_rfq=on_entry_rfq,
        on_exit_rfq=on_exit_rfq,
        events=ManagedMakerEventHandlers(on_entry_filled=on_fill, on_exit_filled=on_fill),
        on_alert=on_alert,
        funding=ManagedFundingOptions(),  # plans each funding deadline and raises the alerts; moves no money
    )
    await maker.run()


asyncio.run(main())

Olive's onboarding test request is answered for you with the 5 USDC minimum premium; pass on_test_rfq only to answer it differently (below).

Deposits by hand

This is the default. The bot moves no money. Your desk deposits, and the SDK tells it when and how much.

funding=ManagedFundingOptions() turns on the deadline planner. For each expiry it works out what your margin account needs by the funding deadline, which is 24 hours before expiry. It raises funding_due 36, 30 and 25 hours before that deadline, and funding_overdue from the deadline while the money is missing. true_up_pending means an expiry settled short and you owe the gap. Send all of them to your own alerting from on_alert.

In every one of these alerts, data.amount is the USDC to deposit in base units and data.deadline is the deadline in unix seconds. For example, amount "250000000" is 250 USDC. Deposit that amount from your funding wallet with deposit_pool_instruction, or with the deposit transaction the maker console builds for the funding wallet to sign. Olive counts it at the next refresh and the SDK raises funding_ok.

If a deposit is late, nothing defaults before expiry. From the deadline Olive stops sending you new requests, and the program refuses new sales and withdrawals, until the money is in. Your open positions stay and settle from your margin account. Requests resume by themselves after the deposit lands. See Risks.

The buyback allowance works the same way. Approve it once from the funding wallet with approve_exit_delegate_instruction. The maker console builds that approval transaction for the funding wallet to sign (the Activate stage of onboarding, or Exit approval in the console afterwards). While it is 0, every buyback is declined. buyback_allowance_low says when it is low or a buyback was declined, and then you raise it from the funding wallet. Without a signer, nothing tops it up for you.

Automate deposits

Optional. A funding signer lets the SDK ask for each deposit, and keep the buyback allowance topped up, so your desk only approves. Pick who signs. Start on devnet and watch your first deposit go through.

  • LocalKeypairFundingSigner(keypair, rpc). A key in this process signs and sends. For tests and a small float wallet.
  • FordefiApiFundingSigner(vault_id=...). Your Fordefi vault signs after your Fordefi policy approves; the API token comes from FORDEFI_API_TOKEN and is never logged. The Solana RPC comes from SOLANA_RPC_URL (the environment, else olive.env) unless you pass rpc=; with neither it fails at construction, saying so.

Approval stays with your Fordefi policy: the SDK only submits the request and waits for it to land. Watch the first requests in Fordefi and keep your own alerting on funding deadlines.

With a signer, also let the SDK keep the buyback allowance topped up through it:

from olive_solana import FordefiApiFundingSigner, ManagedExitApproval, rpc_token_account_reader

FUNDING_SIGNER = FordefiApiFundingSigner(vault_id="...")  # builds its RPC from SOLANA_RPC_URL

maker = ManagedMaker.from_env(
    ...,  # the four functions, as above
    funding=ManagedFundingOptions(signer=FUNDING_SIGNER),        # asks custody for each deposit the pool needs
    exit_approval=ManagedExitApproval(signer=FUNDING_SIGNER),    # keeps the buyback allowance up, also through custody
    funding_wallet_account=rpc_token_account_reader(RPC_URL),    # the top-up reads the allowance and balance on chain
)

What the SDK does with a signer:

  • Nothing moves without your approval. Olive builds an unsigned transaction, the SDK checks it instruction by instruction, and your signer takes it to custody. Olive never holds a maker key.
  • Pinned mint. Prepared transactions are checked against the deployment's USDC mint (OLIVE_USDC_MINT); the source (ATA(signer, mint)) and the margin account are derived, the instruction is found by program id and Anchor discriminator, and a durable nonce is required unless the signer sends at once (requires_durable_nonce = False).
  • No double counting. Every request still waiting, including a timed-out one (custody may still approve it), counts as money on its way and is subtracted from the next gap; it is watched until it lands, is refused, or abandon_after_ms (7 days) passes. Each request has a deterministic reference that the Fordefi signer sends as x-idempotence-id.
  • Single instance. Pending requests are held in the process: run one ManagedMaker per MakerConfig.
  • Buffer. max(gap * buffer_bps / 10_000, min_buffer_usdc) on top of the gap (200 bps, 1 USDC), or a fixed buffer_usdc; a request below min_request_usdc (1 USDC) waits for the next stage.
  • Buyback allowance. The top-up restores the allowance to target_usdc (default 25,000 USDC), never above it, decided from the chain via funding_wallet_account (skipped, exit_approval_failed, without it), and adds at most the 24 h refill budget (below).
  • Defaulted or inactive maker. No deposit requests, one funding_overdue alert (data.blocked); overdue alerts repeat hourly for 6 hours, then every 6 hours.
  • For a separate hot float wallet, add auto=AutoFunding(float_signer, float_wallet, max_per_day_usdc) to ManagedFundingOptions; it is off unless set.

Alerts

code When data
funding_due 36, 30 and 25 hours before a funding deadline amount, deadline, reasons, onTheWay, request
funding_overdue from the deadline while money is missing (hourly for 6 hours, then every 6 hours) as funding_due; blocked when the maker is defaulted or inactive
funding_ok an alerted need is covered need
true_up_pending an expiry settled short: pay within 4 hours amount, deadline, bucket
credit_breached a price move took you over your credit line; sales that need new credit wait amount (over the line), creditLimit, unfundedExposure
routing_paused Olive stopped sending you entry requests reason, amount, maturity, bucket
not_receiving_rfqs Olive says why you are not getting entry requests reasons, markets
buyback_allowance_low the allowance is low, its refill budget is used or a refill failed, or a buyback was declined current, wanted or needed and approved
handler_failed your pricing handler threw or ran past pricing_timeout_ms; the RFQ is declined (pricer_error, too_slow). Also: an event handler failed and is retried rfqId, kind, reason, declineReason; for an event method, reason, blocksEntries
quote_invalid your handler returned a quote the SDK will not sign (premium not a whole number of USDC base units, under the 5 USDC minimum, exit amount not positive); declined invalid_quote rfqId, kind, reason, declineReason
unquoted_exit an exit was paid that your bot didn't quote: your bot key may be compromised (critical) amount, rfqId, quoteId, position, transactionSignature, predatesProcess
fee_sol_low at start: the funding wallet holds less than 0.05 SOL for transaction fees wallet, lamports, minimumLamports
funding_wallet_unreadable at start: the funding wallet's USDC account could not be read exitSource, reason

Amounts are USDC base units and deadlines unix seconds, as decimal strings; data keys are camelCase in both SDKs. The same alert (same key) repeats at most once an hour (alert_repeat_ms). The plain log lines stay (stderr, or logger=). deadline_roll_pending and underfunded_past_deadline are reserved codes, not sent on V13.

Every alert has an action: one short sentence saying what to do now (for example Approve the pending deposit in your custody.). When a deposit request is already waiting in custody, funding_due and funding_overdue say so instead of "deposit now", and data.waitingOn is "custody". A pricing failure never holds you out of new entries (only an event handler that keeps failing on bucket.funding_required or bucket.true_up_pending does, and handler_failed says so with data.blocksEntries). unquoted_exit is the stolen-key alarm: revoke your buyback allowance first, then replace your bot key. Set alarm_on_unquoted_exits=False only if several processes quote with one bot key.

Default log lines

The SDK prints one plain line (to stderr) for each of: connected, signed in, synced, disconnected (with the reason: "Olive is restarting", "login refused"), reconnecting, every RFQ received (id, asset, side, call or put, strike, size), every quote sent (with the price) and decline sent, every fill, and every alert, plus the money events above. No keys or signatures appear in them except transaction signatures. Redirect them with logger=lambda entry: ... (entry.level, entry.message, entry.source) or turn them off with quiet=True. Pricing code gets pricing_timeout_ms (default 1500 ms; the auction window is 2000 ms); 0 turns the timeout off.

Errors and start-up checks

Without on_error the SDK prints every error it reports, with the reason, through the same default log (an error line, for example Error: FORBIDDEN: ...), so a bot that cannot start says why; and when the gateway refuses a startup call the Disconnected from Olive: code 1011 (STARTUP_FAILED - ...) line carries the answer. With on_error set you get the error and the SDK prints nothing extra. A session subscribes to the assets it claimed (OLIVE_ASSETS) unless you give topics; OLIVE_TOPICS is still read, but when it differs from OLIVE_ASSETS the bot logs one warning that it is a duplicate and will be removed in 4.0.

Every time it starts, before it connects, the managed maker also checks itself: it reads Olive's API, your Solana RPC (rpc_url, else SOLANA_RPC_URL), your maker account and your funding wallet. It does not start, and says why in plain words, when olive.env names another network or deployment than Olive serves, when your Solana RPC is on another network or has no Olive program, or when your signer's public key isn't the one registered for this maker. Everything else is a STARTUP CHECK warning in the log and the bot connects anyway: a low buyback allowance, too little SOL on the funding wallet for transaction fees (0.05 SOL), a funding wallet that cannot be read, no Solana RPC set. The low-allowance, low-SOL and unreadable-wallet warnings are also alerts (buyback_allowance_low, fee_sol_low, funding_wallet_unreadable). These checks run by themselves; there is nothing to call. olive-maker-doctor stays an optional check you run by hand and, given --rpc-url, runs the same checks.

A plain def pricing handler runs on the event loop and cannot be interrupted: one that finishes after the limit is declined too_slow. Use async def (and asyncio.to_thread for CPU-heavy pricing) so the SDK can cancel it.

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).

How margin works (reference)

You do not need this to write a bot; it is what Olive and the program do with your money.

  • Legs. Entry RFQs, quotes and positions carry legs: {kind, maturity, strikeE8, qty} (kind 0 = call, 1 = put; qty the unsigned size the user buys). The wire carries 1 to 4 legs; the program accepts exactly one user-long call or put. ManagedMaker declines any other structure itself (unsupported_structure) before your code sees it, and rfq.leg (LaunchLeg: kind, kind_name, strike_e8, size, maturity) is the leg to price. assert_launch_structure / launch_leg raise LegError with the program's error name. Maturities sit on the 08:00 UTC grid (is_maturity_on_grid): an off-grid leg fails InvalidMaturity and ManagedMaker declines its RFQ as invalid_maturity.
  • Signed quotes. Entry quotes sign under OLIVE_ENTRY_QUOTE_V13 (the leg, the unsigned premium); exits under OLIVE_EXIT_QUOTE_V13 (the unsigned amount, fractionBps of the original position and the position's remainingBps before the exit; no USDC source). You return the quote's premium and amount; 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 margin account, one balance. Buckets hold no tokens. Every maker has one margin account, the USDC associated token account of its MakerConfig (pool_vault(maker_config, mint)), and the maker ledger splits it into pool_free (the maker's to withdraw) and pool_committed (the collateral of 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 with withdraw_pool_instruction (unrepaid insurance is repaid first; amount=0 only 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 margin covers the rest, and collateral above the requirement goes back to pool_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, defaulted and needed_by_next_deadline (what to deposit before next_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_funding predicts the margin account pull and both refusals; capacity_entry_funding and capacity_max_size(capacity, market_config=..., maturity=..., kind=..., strike_e8=..., max_size=..., now_seconds=...) run it on a maker.capacity result so you can clip a quote to what will execute (max_size_for_quote underneath). 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 margin account movement: the funding wallet's USDC account (exit_source_address(funding_authority, usdc_mint)) approves the PDA exit_delegate_address(program, maker_config) with an SPL approve (approve_exit_delegate_instruction, signed by the funding wallet, never the bot key; revoke_exit_delegate_instruction stops every exit), and accept_exit_quote pays through it. The buyback allowance falls with every exit and is the cap on what a stolen bot key could move: size it to a day or two of exit flow, not to the wallet. read_exit_approval / check_exit_approval tell you where it stands (ExitDelegateNotApproved, ExitAllowanceExceeded). The SDK declines (it does not raise) an exit quote above ledger.exitDelegatedAmount, above the funding wallet's balance (with funding_wallet_balance=), or while insurance is unrepaid: it sends rfq.decline with the reason exit_approval_too_low, exit_wallet_balance_low or insurance_unrepaid and reports exit_declined (amounts, balance, what to do). Pass exit_approval=ManagedExitApproval(funding_authority=..., submit=...) to ManagedMakerOptions and 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, it restores the buyback allowance to the target (never above it) through signer= (custody, see "Automate deposits") or a submit(instruction, current=..., target=...) callback that signs with the funding wallet, one request at a time and at most once a minute (exit_approval_set / exit_approval_failed statuses). Without the option it reports exit_approval_low and the buyback_allowance_low alert.
  • The refill budget. The auto top-up refills the buyback allowance each time it drops below half. On its own that would turn the buyback allowance into no limit at all: an attacker who signs exits with a stolen bot key makes the bot refill the allowance after every one. So the SDK adds at most max_refill_usdc_per_24h to the buyback allowance in any rolling 24 hours (default: twice the target, 50,000 USDC for the default target; the first buyback allowance at start-up counts). When the budget is used it stops refilling and reports exit_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; raise max_refill_usdc_per_24h if 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.filled is compared with the exit quotes this process signed. A fill against a quote it did not sign raises the unquoted_exit status and an EXIT YOU DID NOT QUOTE line 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 bot key signed elsewhere: revoke the buyback allowance first, then rotate the key. If several processes quote with the same signer, set alarm_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, margin account deposited and withdrawn (a withdrawal is a WARN: it used the maker-authority key), routing paused and resumed, and every buyback allowance status. A line carries amounts in USDC, the transaction signature and the new balance where known, for example FUNDING NEEDED: deposit at least 12,345.67 USDC into your margin account within 3h 20m (deadline 2026-10-17 08:00 UTC, bucket 7Hk2...4fQa). Lines go to stderr; pass logger= to send them elsewhere.
  • Nothing due. Entries are refused while the maker has a Pending bucket (PendingBucketsOutstanding; the free margin is then held for the true-up and reprices pull nothing from it) or a live bucket past its maturity (EarlierMaturityOutstanding); capacity_max_size returns 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 (originMaker vs makerConfig on 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 margin 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_required at 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 and secondsUntilDeadline (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.capacity reports routing: {paused, reason?, since?} on both scopes (reason and since absent, not null, when not paused) and maker.routing_paused / maker.routing_resumed announce it. Exits, funding, true-up and settlement continue. ManagedMaker turns these notices into the funding_due / funding_overdue and routing_paused alerts (with the amount and the deadline) without any handler, and with a funding signer it asks your custody for the deposit (see "Automate deposits").
  • 4h true-up. A bucket that settles short enters pending with a 4-hour true-up window (bucket.true_up_pending, true_up_deadline; anyone may fund_bucket). Afterwards the margin account, then insurance (insurance.drawn) pay, and holders are paid pro rata (haircut); the maker defaults.
  • Headroom. maker.capacity (scope all) returns the maker ledger (poolVault, poolFree, poolCommitted, creditLimit, unfundedExposure, availableCredit, breached, defaulted, insuranceDrawn, insuranceUnrepaid, pendingBucketCount, exitSource, exitDelegatedAmount, maturities[] and underfundedPastDeadline), the margin constants, each market's risk parameters, minLegNotional and live price, and the maker's one bucket per expiry (required, collateral, unfunded, payoutPool, payoutRemaining, live, deadlines and occupied slots {index, kind, strikeE8, qtyShort}). Scope ledger returns only the ledger and routing. credit.breached reports unfunded exposure above the limit after a price move; credit.limit_changed a 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.

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) (optional: maker.book keeps every active position) 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 pages maker.positions (active) to the end and reads maker.capacity. OliveSnapshot has slot, positions (a mapping keyed by position address), capacity, connection_id and sequence. A sync that fails, or an on_sync that raises, is retried with backoff (sync_failed status); nothing is dispatched until one succeeds. RFQs wait for the sync until their auction ends (rfq_skipped otherwise).
  • snapshot.slot is 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 has transactionSignature) 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) (and client.position(address)) reads one of the maker's positions, in any status, through the maker.position method.
  • 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 with ManagedMakerOptions.limits); the event buffer holds event_queue_capacity events and leaves the rest unacknowledged for redelivery (event_dropped).

Remote and HSM signing

The bot key file that from_env reads (OLIVE_QUOTE_SIGNER_KEYPAIR) is for laptop tests or a tightly controlled key-file deployment. Production remote/HSM integrations pass ManagedMaker.from_env(signer=...) a CallbackMakerSigner and never give 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_ACTIVE names the sessions in the way in conflicting_assets on the MakerWsRpcFailure (the terminal error's __cause__) and in the session_release_wait status. takeover_existing_session=True replaces 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.capacity shows ledger.reservedCredit across all sessions, while its other rows and maker.positions are filtered to the session's assets.
  • Stagger process starts by about one second. FORBIDDEN with session_limit reports maxSessionsPerMaker; asset_not_claimed means 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 wallet for fees), then, in the page's Activate stage, takes the account over (accept_maker_authority), deposits into your margin account (deposit_pool, signed by the funding wallet) 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 margin account: a rerun of the same command leaves a funded margin account alone (it also reads the margin account 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 sets the buyback allowance on the funding wallet's USDC account with an SPL approve of the exit delegate (what exits may pay from the funding wallet, not from the margin account; 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 margin account. 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 wallet's USDC account into your maker's margin account, or Olive's accept_maker_authority for your maker, the signing key the only signer and fee payer (the buyback allowance 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: --keypair stays 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-keypair it also adds their signatures as extra proof). It cannot sign for keys it does not hold, so in separate mode:
    • the margin account 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 wallet; refused otherwise); the buyback allowance, also signed by the funding wallet, waits for it;
    • the readiness check runs only with --quote-signer-keypair <path> (it must match the bot key); otherwise the command prints what to run on the quoting server;
    • olive.env names --quote-signer-keypair as OLIVE_QUOTE_SIGNER_KEYPAIR when 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 wallet, so they arrive without its key.
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 buyback allowance 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 buyback allowance (fail below the buyback allowance, 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 bot key, otherwise it is neither dispatched nor acknowledged (event_rejected).
  • quote.selected and exit_quote.selected are not fills. Only finalized trade.filled and exit.filled are canonical execution signals.
  • Signatures and private signer data are never included in SDK status events.

Releases must pass the shared fixture/digest suite and all fail-closed maker conformance scenarios.

Advanced: raw events

Most makers never need this section: the alerts above already cover every money event. It is for desks that want to react to the raw lifecycle events themselves, or run their own orchestration.

ManagedMakerEventHandlers has a handler per event, besides on_entry_filled and on_exit_filled: on_funding_required, on_true_up_pending, on_credit_breached, on_routing_paused, on_routing_resumed, on_bucket_funded, on_bucket_refreshed, on_bucket_settled, on_pool_deposited, on_pool_withdrawn, on_insurance_drawn, on_credit_limit_changed, on_maker_status_changed, on_maker_default_cleared, on_market_updated, on_bucket_handed_over, on_settlement, on_position_redeemed, on_position_expired, on_entry_selected, on_exit_selected (selection is feedback, not a fill), the two ..._expired_unfilled, and a catch-all on_event. on_sync(snapshot) runs on start and after every reconnect, before anything else of that connection (see "State without a database"); maker.book keeps every active position without it.

bucket.funding_required and bucket.true_up_pending put money at risk: Olive refuses the maker new entries while one is unacknowledged. The SDK acknowledges them for you when on_alert or funding is set (the alert is how they are handled). Without either (a 3.1 bot) they need on_funding_required / on_true_up_pending or the generic on_event; otherwise the event is never acknowledged. Every money event is also logged with its amounts before any handler runs.

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.
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.

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.

To deposit from your own code instead of a FundingSigner, build deposit_pool_instruction for the alert's amount (or maker_balance(capacity).needed_by_next_deadline), sign it with the wallet the USDC comes from, and send it. capacity_max_size(capacity, ...) sizes a quote against your credit and free margin if you want your own limit on top of Olive's checks. on_status reports every SDK status (funding_warning, routing_paused, exit_declined, unquoted_exit, quote_rejected, ...).

The low-level MakerWsClient remains available for makers requiring custom orchestration: it speaks the wire protocol and leaves quoting state, acknowledgement and recovery to you.

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.2.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for olive-solana-sdk 3.2.3
File Size Uploaded Provenance
olive_solana_sdk-3.2.3.tar.gz 333.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for olive-solana-sdk 3.2.3
File Interpreter ABI Platform
olive_solana_sdk-3.2.3-py3-none-any.whl Python 3 none any Details

Total release size: 553.7 kB

Release files / olive_solana_sdk-3.2.3.tar.gz

Download URL olive_solana_sdk-3.2.3.tar.gz
Size 333.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5ef2d2171b2f31ebeea12243c03513e22677f8518ae076dccc6f970b9722722f
BLAKE2b-256 checksum
How to use checksums
2efeb0c1449720472d9f6397f528e29b45a76e875a33d0ff9b3706c02412420a
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 22, 2026.

Transparency log

Release files / olive_solana_sdk-3.2.3-py3-none-any.whl

Download URL olive_solana_sdk-3.2.3-py3-none-any.whl
Size 220.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8db33e7d95b1d39d5adb939314797ecddfb33cc4d01e6f175e47253e4927f3de
BLAKE2b-256 checksum
How to use checksums
e1f4ddf393a2306fb2f8bbdba09d31e57eac94f4c16869f1a17b880803b30213
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 22, 2026.

Transparency log

Security for olive-solana-sdk 3.2.3

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for olive_solana_sdk-3.2.3.tar.gz

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 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page