Skip to content

How trading works ​

Papersol has one order type: a market order that fills in two steps, at the one oracle update selected by a pin time set when the order lands.

Submitting an order commits you to that pinned price, as long as the update is admissible. Orders have no limit price, slippage tolerance or minimum payout, and an adverse price move doesn't let you cancel.

Positions ​

  • Each position is isolated: it has its own margin, market, side and leverage. notional = margin × leverage.
  • Leverage is a whole number from 1 to the market's max_leverage (currently 1000).
  • You can hold any number of positions, in the same market or different ones. They never net against each other.
  • A position is opened and closed in full. The program has no partial close, no way to add margin, and no limit, stop or take-profit orders.
  • There is no funding rate, borrow fee or expiry. A position stays open until you close it, it is liquidated, or its market is settled.

Lifecycle of an order ​

StepWhoWhat happens
1. RequestYou (wallet or session key)request_open or request_close creates a request account and fixes its pin time pin_ts. You escrow SOL for rent and the execution fee. For an open, the margin moves from available to locked and the notional is reserved against the open-interest cap.
2. Wait for the pinNobodyNo fill is possible until the Solana clock reaches pin_ts.
3. FillAnyone, normally the Papersol keeperThe filler posts the Pyth update pinned to pin_ts on Solana and calls execute_open or execute_close. The program checks the update and settles.
4. Expiry (only if unfilled)AnyoneAfter expires_ts the request can no longer fill. Anyone can then cancel it. Cancelling an open refunds its locked margin; cancelling a close leaves the position open. Either way most of the escrowed SOL is refunded. Expiry alone changes no accounts: until someone cancels, the request and its effects stay in place.

The pin ​

pin_ts     = max(clock, last_oracle_ts + 1) + pin_delay_s
expires_ts = pin_ts + fill_window_s
  • clock is the Solana clock (unix seconds) when the request lands.
  • last_oracle_ts is the newest verified Pyth publish time the program has recorded for the market. Fills, liquidations and heartbeats all record it, and it only moves forward.
  • pin_delay_s is currently 6 s and fill_window_s is currently 30 s. Both are governance parameters.

A fill must use the unique Pyth update with prev_publish_time < pin_ts ≤ publish_time: the first update published at or after the pin. Exactly one update meets this condition, so neither you nor the filler can choose the price. The fill also requires that:

  • the Solana clock is between pin_ts and expires_ts;
  • the update is fully Wormhole-verified, for this market's feed, and admissible (confidence and EMA limits, as recorded in the request when it was made; see Oracle and safety);
  • the update was published no more than 60 s ahead of the Solana clock.

Why fills take a few seconds, and why that protects the house ​

The earliest a fill can happen is pin_ts, at least 6 s after your request lands. After that, a keeper has to fetch the pinned update from Pyth, post it on Solana and send the fill. No program rule bounds this step. Measured on devnet in October 2026, a Pyth posting landed in a median of about 3 s, with occasional tails of 7–12 s. The pin can be up to 6 s later than clock + 6 s: if the watermark is 5 s ahead of the clock, pin = clock + 5 + 1 + 6.

The delay blocks latency arbitrage. Without it, a trader who sees a price move first, on a faster venue or in a newer Pyth update, could trade at an older on-chain price that is already stale and collect a risk-free win. Pinning always stops anyone from choosing among published updates: exactly one update qualifies. The fill price is published at or after max(clock, last_oracle_ts + 1) + 6 s. That is later than any price you could have seen, provided the Solana clock or the market's latest oracle observation is close to real time. The program compares the watermark with the Solana clock, not with true real time. If the clock lags and the watermark is old, the pin can land on a price that has already been published. A request and its fill can never land in the same transaction or bundle, because the fill requires clock ≥ pin_ts.

Like other Solana perps, Papersol trusts the Solana clock (measured within about 1 s of real time on mainnet). There are extra layers for the case where it lags:

  • The pin is never earlier than last_oracle_ts + 1 + pin_delay_s. Every fill records a fresh price, and on a quiet market the keeper records one every 30 s.
  • The keeper compares the Solana clock with real time every 2 s and alerts the team if the clock falls more than 2.5 s behind, so new opens can be paused or heartbeats sped up.
  • If a verified Pyth update is more than max_clock_lag_s (5 s) ahead of the Solana clock, new requests are refused.

A fill at an already-published price needs both a lagging Solana clock and a watermark at least pin_delay_s + 1 seconds old in real time. The keeper still fills such requests, because leaving them unfilled would give the trader an option, and it raises an alert.

What a request needs ​

CheckApplies toError
Protocol launchedopenNotLaunched
Signer is the owner or a live session keyopen, closeUnauthorized
Oracle watermark fresh: clock − 60 s ≤ last_oracle_ts (max_obs_age_s)open, closeStaleHeartbeat
Oracle not too far ahead of the clock: last_oracle_ts ≤ clock + 5 s (max_clock_lag_s)open, closeClockLag
Not paused; market not retiredopenPaused, MarketRetired
1 ≤ leverage ≤ max_leverageopenLeverageTooHigh
margin ≥ min_margin; notional ≤ max_position_notionalopenMarginTooSmall, PositionTooLarge
Side open interest (live + pending + this order) within the capopenOiCap
Your open plus pending notional within max_trader_notionalopenTraderNotionalCap
margin ≤ availableopenInsufficientBalance
Position has no pending close; market has no settlement price; if a settlement is pending, the close is pinned before its settlement pincloseClosePending, MarketSettled, SettlementPending

Closes are allowed while the protocol is paused and in retired markets.

execute_open re-checks the pause, retirement, leverage, minimum margin, maximum notional, trader cap and OI ceiling and brake. If governance or the guardian tightened one of them after you placed the request, that fill attempt fails but the request stays pending. It can still fill before expires_ts if the condition clears (for example, the pause is lifted). Otherwise it expires, and cancelling it returns your margin and escrow.

Expiry, cancellation and refunds ​

  • Ordinary cancellation requires expiry: cancel_request fails at or before expires_ts for everyone, including you, the keeper and the admin. If a trader could cancel after seeing the pinned price, every order would be a free option against the house. The one exception is void_close (below), which needs proof that a close's pinned update is inadmissible.
  • Revoking or expiring a session key doesn't cancel requests it already placed. Accepted requests can still fill until they expire.
  • After expires_ts, anyone can call cancel_request:
    • Open: the margin goes back to available, and the OI reservation and your pending notional are released.
    • Close: the position stays open with its margin. The pending-close marker is cleared, and you can request a close again. Until the expired close is cancelled, its marker stays and still affects liquidation (Liquidation). If the position was already liquidated, the leftover close request still needs cancelling to recover its escrow. (A settled market's position with a pending close isn't settled until that close is filled, voided, or expired and cancelled.)
    • SOL: the caller receives ¼ of the execution fee (125,000 lamports at current settings). Everything else, meaning the rest of the fee, the request rent and any escrowed position or claim rent, goes back to whoever paid for the request. If you cancel your own request, you get all of it back minus the transaction fee. The keeper cancels expired requests automatically.
  • A request reaches expiry when nobody fills it by expires_ts. That happens when no filler acts, when a re-checked limit keeps failing until the deadline, or when the pinned Pyth update is inadmissible.
  • If a close's pinned update is inadmissible, anyone can call void_close right away instead of waiting for expiry. That closes the request, clears the pending-close marker, and refunds the request rent and escrowed claim rent. The execution fee goes to whoever posted that update.

SOL you pay per request ​

ItemOpenCloseWhat happens to it
Request account rent≈ 0.0019 SOL≈ 0.0019 SOLRefunded to the request's payer at fill, cancel or void
Execution fee500,000 lamports (0.0005 SOL)sameFill: paid to whoever posted the pinned Pyth update. Expiry: ¼ to the canceller, ¾ refunded.
Position rent escrow≈ 0.0019 SOL—Reimburses the filler, who creates the position account. Refunded to you when the position closes, is liquidated or is settled.
Queue-claim rent escrow—≈ 0.0016 SOLRefunded at fill unless the win is queued. If it is queued, refunded when the claim is fully paid.

Rent figures are Solana's rent-exempt minimums for each account size, so they move with Solana's rent rate. Transaction fees come on top.

The execution fee goes to the poster of the pinned price update (its write authority), not necessarily the filler, because posting the update is the expensive part. If the poster's account is empty and the fee alone wouldn't make it rent-exempt, the filler gets the fee instead.

The request's rent payer is whoever signed the request. If a session key signed it, every refund goes to the session key's address.

One pending close per position ​

A position can have only one pending close. While it is pending, a liquidation can only use a price published before that close's pin: the close you committed to first takes precedence (see Liquidation).

Entry and exit prices ​

  • Entry is the pinned Pyth aggregate price, normalized to 8 decimals and rounded down. Exit is the pinned price of your close request.
  • No spread and no size-based slippage are added to entry or exit. The cost of a win is the haircut below.
  • If the exit price is at or past your bust price, the close settles as a liquidation and you lose the whole margin (see Liquidation).
  • Crowded-side averaging is compiled out of production builds. The code can price requests on a crowded side at the median of several pinned updates, but production builds reject any market configuration that enables it (crowd_oi must be u64::MAX), and every request uses exactly one pinned update (samples = 1). The max_samples argument of request_open and request_close has no effect in production. Turning averaging on would take a program upgrade.

Price impact: the win haircut ​

Price impact in Papersol doesn't move your entry or exit price. It reduces winning PnL only. With m = |exit − entry| / entry for a winning close:

adjusted = notional × max(0, m − deadband) × (1 − base_rate) × m / (m + c)

The program evaluates this in exact integer arithmetic, scaled by 1e9, and rounds down at each step:

d9       = dist × 1e9
eff9     = d9 − entry × deadband_e9            (adjusted = 0 if d9 ≤ entry × deadband_e9)
gain     = floor(notional × eff9 / (entry × 1e9))
adjusted = floor( floor(gain × (10000 − base_rate_bps) / 10000) × d9 / (d9 + entry × c_e9) )

Current parameters, the same for BTC, ETH and SOL and changeable by governance:

ParameterValue
base_rate_bps770 (7.70%)
c_e9185,000 (c = 0.000185 = 1.85 bps)
deadband_e931,000 (0.000031 = 0.31 bps)

Properties:

  • Losses are never haircut.
  • The continuous formula doesn't depend on size: the same move keeps the same fraction of raw PnL at $10 or $10M notional. The integer rounding steps can change the exact fraction slightly, most noticeably on very small amounts.
  • Small moves are haircut the most. The share of raw PnL you keep, before the 2% payout fee:
Move in your favour1 bp5 bps10 bps1%5%limit
Kept (adjusted / raw)22.3%63.2%75.5%90.3%91.9%92.3%
  • Moves of 0.31 bps or less pay nothing.

Open-interest limits ​

Each side of each market caps the live + pending notional:

LimitSet byCurrent valueApplies to
Ceiling oi_cap_long / oi_cap_shortGovernance (timelocked)$5,000,000,000 per side on devnet; mainnet: TBD (the runbook says start very low)New requests and fills
Trailing limit oi_limit_*Risk keeper, instantly, anywhere between current usage and the ceilingThe keeper aims for usage + $5,000,000 and re-targets once the headroom leaves ±10% of thatNew requests only
Emergency brake oi_brake_*Admin or guardian lower it instantly; only the admin raises or removes itOffNew requests and fills
  • New request: live + pending (including this order) ≤ min(trailing limit, ceiling, brake).
  • Fill: live + pending ≤ min(ceiling, brake). A routine change to the trailing limit never breaks an order that was already accepted. A brake or a lowered ceiling can: that order expires and is refunded.
  • Pending opens reserve capacity when they are accepted, so nobody can block other traders' fills by occupying the cap and releasing it selectively.
  • Closes are never blocked by OI limits, and existing positions are never affected by them.
  • Only the on-chain limits constrain an order. The keeper aims for $5M of headroom and re-targets when it leaves the ±10% band, checking about every 5 s. Actual headroom can be outside that band in between: limits start at the ceiling when a market is created, and closes free up headroom. So an order up to max_position_notional ($10M) may or may not be admissible at a given moment.

Other limits ​

LimitCurrent valueSet by
min_margin$1Governance, per market
max_position_notional$10,000,000Governance, per market
max_trader_notional (open plus pending opens, all markets)$10,000,000Governance

Mainnet values: TBD. All of these are re-checked at fill.

Session keys (1-click trading) ​

A session key is a second keypair that you delegate trading to, so trades don't need a wallet pop-up.

  • set_session(key, expires_ts) is owner-only. There is one session key at a time; setting a new one replaces the old. The expiry must be in the future and no more than 30 days away, and the key can't be your own wallet. revoke_session (owner-only) removes it immediately for new actions. Requests the key already placed can still fill until they expire.
  • The program accepts a signer if it is the owner, or if it is the session key, the key is set, and now < expires_ts.
A session key canA session key cannot
request_open, request_closewithdraw
stake, unstake, claim_rewards (rewards go to your available balance)deposit
Anything permissionless that any wallet can doset_session, revoke_session, create_trader
  • A session key can trade your whole available balance, not only what you deposited when you turned it on. A leaked key can't take your USDC, but it can open and close positions, including losing ones, until it expires or you revoke it.
  • The session key pays the rent and execution fees of the requests it signs, and those refunds go back to the session key's address.
  • In the Papersol app, the session key is generated in your browser and stored in its local storage. It is valid for 7 days and funded with 0.05 SOL. Turning 1-click off revokes the key and moves its SOL back to your wallet. Rent refunds from requests the key signed keep arriving at the key's address afterwards; you can recover them the same way.

Papersol is experimental software. Trading with leverage can lose your whole margin.