Instant Close
Instant Close is the lowcap flow for closing (fully or partially) an open position. Same shape as Instant Open — sign, submit, get an id — with a bulk variant for closing multiple quotes atomically.
The SDK splits the flow into three functions on purpose (same pattern as Instant Open):
prepareInstantCloseParams— resolve every derivable input (market metadata, mark price) into the exact wire-shapedInstantCloseParametersbag.instantClose— sign + submit. Requires the fully-resolved bag.instantCloseAuto— thin wrapper that callsprepareInstantCloseParamstheninstantClose. One line for callers who don’t care about the intermediate state.
Latency implication: instantCloseAuto runs the prepare fetches inline, so total wall-clock is prepare fetches + sign + POST. Call instantClose directly (with a pre-resolved bag) when close speed matters — e.g. closing on a fast-moving market where the mark-price fetch would burn the price window.
Bulk close (instantCloseBulk / instantCloseBulkAuto) mirrors the pair: one atomic solver call across multiple quotes, with the same prepare-first / submit-second split.
Close order types
Market closes select their contract order type from the resolved solver before encoding and signing requestToClosePosition:
| Close request | Encoded order type |
|---|---|
| Enigma market close | OrderType.MARKET_BEST_EFFORT (2) |
| Rasa market close | OrderType.MARKET (1) |
| Limit close | OrderType.LIMIT (0) |
This applies to instantClose, instantCloseAuto, instantCloseBulk, and instantCloseBulkAuto. The caller’s solverId override takes precedence over the chain’s default solver.
For instantClose, omit orderType or pass ORDER_TYPE_MARKET (1) for a market close. Both select best-effort execution on Enigma. The input remains 0 | 1; callers do not need to pass 2. The resolved value is part of signedOperation.callData, covered by the user’s signature, rather than a separate HTTP request field.
Enigma’s best-effort mode allows the solver to use fillCloseRequestToLiquidation when only part of the requested quantity can be closed within the protocol’s solvency boundary. After that partial fill, the contract cancels the unfilled close request remainder and leaves the remaining position open. Prefer the SDK market-close actions when constructing Enigma closes so they select this mode automatically. See OrderType for the contract behavior.
Fees at close
Instant Open defers close fees — the open funds only its open-side legs, so the close fees are charged here, at close, by the contract collecting them from the position. Three legs:
- Platform close fee — taken from the position’s allocated balance after PnL settlement.
- Solver close fee — priced by the position’s real holding time (the time-decaying rate that starts at the early rate and decays to the standard rate), and capped by the
closeRateCapthe quote signed at open. A longer hold pays less. - Static close fee — the flat USD close amount from the solver’s
getSolverInfo(charged on each partial close too).
Liquidation charges no fees. These close fees are collected only on a user-initiated close, not when a position is liquidated.
Deferral is safe precisely because the contract already collects close fees at close — pre-funding them at open only stranded collateral. Preview these legs with getInstantCloseFees, which prices them from the current mark price and the position’s real holding time. The open-side preview (getInstantOpenFees) deliberately covers only the legs the open funds.
Import
import {
instantClose,
instantCloseAuto,
limitCloseAuto,
prepareLimitCloseParams,
instantCloseBulk,
instantCloseBulkAuto,
prepareInstantCloseParams,
getInstantCloseFees,
getInstantCloses,
type GetInstantCloseFeesReturnType,
type PrepareInstantCloseParameters,
type PrepareLimitCloseParameters,
type InstantCloseParameters,
type PendingInstantClose,
} from "@symmio/trading-core";The three-function split
Under the hood, instantCloseAuto is:
export async function instantCloseAuto(config, parameters) {
const resolved = await prepareInstantCloseParams(config, parameters);
return instantClose(config, resolved);
}Same reasoning as Instant Open — the wrapper exists for one-line call sites; the split exists so you can:
- Cache the prepared bag (render a review / confirmation screen, submit only after user confirms).
- Override resolved fields (pin a specific mark price you fetched elsewhere).
- Run prepare early — kick off the fetch when the user opens the close panel; when they hit Close, only sign + POST runs.
- Inspect the exact numbers being signed before the network hop.
instantClose on its own is the “I already have everything, just submit” primitive.
prepareInstantCloseParams
Pure resolver. Takes the minimal UI-shaped inputs (partyA, market, side, quote id, quantity, slippage) and returns the full InstantCloseParameters bag needed by instantClose.
Steps (in order):
- Resolve market metadata + mark price — caller-supplied values short-circuit the fetches.
- Compute the slippage-adjusted close price via
calculateClosePrice. - Clamp
quantityToCloseto the market’squantityPrecision. - Convert decimal strings to 18-decimal-wei
bigint.
Slippage is a required input — source it from the user. slippage is a percent tolerance applied to the mark
price to compute the close fill price (calculateClosePrice); thin or fast-moving markets need more of it. Prefer a
user-controlled slippage field. If you must hardcode a default, use 5% or higher — a tight value like 1%
frequently fails to fill on a market close (the price moves past your limit before the solver fills). The examples
below use 5 for that reason.
const resolved = await prepareInstantCloseParams(config, {
from: sessionKey,
partyA,
market: { id: 1 },
positionType: PositionType.LONG,
quoteId: 42n,
quantityToClose: "0.5",
slippage: 5,
});Parameters
Required = inputs only the caller can know (partyA, quote id, close intent). Optional = anything derivable from solver / price-service reads.
Every optional field you pre-fill skips its network fetch. If you already have markPrice from a live price stream, or full precision metadata on market (pricePrecision, quantityPrecision), pass them in — prepareInstantCloseParams short-circuits each field and only fetches what’s still missing. Pass everything derivable and the function becomes pure math with zero network hops.
| Name | Type | Required | Notes |
|---|---|---|---|
from | Address | ✔ | Session-key signer. |
partyA | Address | ✔ | Virtual Account that owns the position. |
market | InstantCloseMarketData | ✔ | { id }, plus optional pre-fetched name, pricePrecision, quantityPrecision. |
positionType | PositionType | ✔ | Side of the position being closed (LONG = sell, SHORT = buy). |
quoteId | bigint | ✔ | Quote id of the position. |
quantityToClose | string | ✔ | Decimal amount to close (partial or full). |
slippage | number | ✔ | Percent (e.g. 1 for 1%). |
markPrice | string? | Pre-fetched mark price (decimal). Omit to fetch via Enigma price service. | |
salt | Hex? | Forwarded to InstantCloseParameters. | |
deadline | bigint? | Forwarded. | |
chainId | number? | Optional chain override. |
Returns
InstantCloseParameters — the full parameter bag instantClose consumes. Inspectable, hashable, safe to log or stage between screens.
Errors
INSTANT_CLOSE_INVALID_PARAMETERS— mark price is zero / NaN, orquantityToCloseis non-positive.
instantClose
Sign the EIP-712 message with config.getWalletClient({ from }) and POST to the solver. The low-latency primitive — no fetching, just sign + network.
const result = await instantClose(config, resolved);Use instantClose directly when close speed matters — typical pattern:
// on close-panel mount, run prepare early:
const prepared = await prepareInstantCloseParams(config, uiInputs);
// user clicks Close — no fetch on the critical path:
const result = await instantClose(config, prepared);Parameters
InstantCloseParameters — the resolved bag from prepareInstantCloseParams. Every field is a concrete value; no fetching happens here.
Returns
Solver response — { success: true; tempCloseId?: string; ... }. Consumers usually just wait for the WS close notification.
Errors
- Solver rejection surfaces as
SymmApiError; inspectresponseDatafor the reason. - Viem
UserRejectedRequestErrorwhen the user dismisses the signature dialog.
instantCloseAuto
Convenience wrapper: prepareInstantCloseParams + instantClose in one call. Same parameter shape as prepareInstantCloseParams.
await instantCloseAuto(config, {
from: sessionKey,
partyA,
market: { id: 1 },
positionType: PositionType.LONG,
quoteId: 42n,
quantityToClose: "0.5",
slippage: 5,
});Latency: prepare fetches run inline, so total wall-clock is prepare fetches + sign + POST. Use the split (prepareInstantCloseParams + instantClose) when close time is critical.
limitCloseAuto
The LIMIT close variant. Same shape and split as the market close, but the caller passes a resting price instead of slippage, and the close is signed with orderType = LIMIT, so the solver writes a pending close that rests at that price rather than filling at mark. Majors / Rasa only — throws UNSUPPORTED_BY_SOLVER on a solver without the limitOrder capability (the same flag that gates a limit open). Deadline defaults to 15 minutes.
const { success } = await limitCloseAuto(config, {
from: sessionKey,
partyA,
market: { id: 1 },
positionType: PositionType.LONG,
quoteId: 42n,
quantityToClose: "0.5",
price: "64000", // resting close price
});Under the hood it mirrors instantCloseAuto — prepareLimitCloseParams then the shared instantClose primitive (which honors orderType = LIMIT):
export async function limitCloseAuto(config, parameters) {
const resolved = await prepareLimitCloseParams(config, parameters);
return instantClose(config, resolved);
}Read the resting close back with the React useLimitCloseAuto hook; it invalidates the instant-closes feed on success so the pending close surfaces immediately.
prepareLimitCloseParams
The LIMIT-close resolver — same steps and inputs as prepareInstantCloseParams but takes the resting price (instead of slippage) and stamps orderType = LIMIT plus the 15-minute default deadline into the resolved InstantCloseParameters bag. Guards supportsLimitOrder and throws UNSUPPORTED_BY_SOLVER off rasa.
instantCloseBulk / instantCloseBulkAuto
Atomic close of multiple quotes in one solver call. Same three-function split — the Auto wrapper is prepare (each) + submit.
await instantCloseBulkAuto(config, {
from: sessionKey,
subAccount,
virtualAccount,
closes: [
{ quoteId: 42n, quantityToClose: "0.5" },
{ quoteId: 43n, quantityToClose: "1.0" },
],
slippage: 5,
});The solver may partially accept — the return value carries per-quote success flags. Consumers should surface partial results rather than treating the batch as all-or-nothing.
For the low-latency bulk path, pre-resolve each per-quote bag via prepareInstantCloseParams (in parallel with Promise.all) then submit via instantCloseBulk.
getInstantCloses
Paginated list of the solver’s pending instant-closes for a SubAccount.
const page = await getInstantCloses(config, {
account: subAccount,
offset: 0n,
size: 20n,
});Consumed by the reconciler (reconcileQuotes), so callers rarely need it directly.
Fee preview — getInstantCloseFees
Preview every fee closing a position pays right now — priced from the current mark price and the position’s real holding time, without signing or submitting anything. This is where the Fees at close legs get their honest numbers: at open the close notional and the holding-time solver rate are unknown (getInstantOpenFees is open-side only), so the preview lives in the close flow. The result is a kind-discriminated union:
- Both kinds —
platformCloseFee(getFeeForUser.closeFee × close notional): the contract charges it on-chain from the allocated balance after PnL settlement. - Enigma (lowcap) adds —
closeSolverFee: the time-decaying close-fee schedule (early → standard) evaluated at the position’s real holding time, capped on-chain by thecloseRateCapthe open signed; plusstaticSolverFeeClose: the flat USD close amount from the solver’sgetSolverInfo(fail-soft — an unreachable/infoprices it"0"instead of failing the preview).
totalFee sums the close legs the kind charges — on majors it is just platformCloseFee. Remember liquidation charges no fees: these are the legs of a user-initiated close only.
Holding time is now − openedAt in seconds, stamped when the function runs. Omit openedAt and the schedule is priced at holding time 0 — the early-rate worst case, the conservative default for a position of unknown age. Pass now to pin the clock for deterministic previews and tests.
const fees = await getInstantCloseFees(config, {
subAccountAddress,
market: { id: 1 },
quantity: "2.5", // leveraged base-asset amount being closed
openedAt: position.createTimestamp, // unix seconds — omit for the worst case
});
console.log(fees.totalFee, fees.platformCloseFee);
if (fees.kind === "enigma") console.log(fees.closeSolverFee, fees.closeSolverFeeRate, fees.holdingSeconds);Parameters
Required = the close intent only. Every optional field you pre-fill skips its network fetch — pass markPrice, feeRates, solverInfo, and full market metadata and the function is pure math with zero network hops.
| Name | Type | Required | Notes |
|---|---|---|---|
subAccountAddress | Address | ✔ | PartyA the platform close-fee rate is read for (getFeeForUser). |
market | InstantCloseFeesMarketData | ✔ | { id }, plus optional pre-fetched name, precisions, and the close-fee schedule fields (hedgerFeeClose, hedgerFeeCloseEarlyRate, hedgerFeeCloseEarlyThreshold, hedgerFeeCloseStandardThreshold, hedgerFeeOpen). |
quantity | string | ✔ | Leveraged base-asset amount being closed (decimal string) — partial or full. |
openedAt | bigint | number? | When the position opened, unix seconds — holding time is now − openedAt. Omit ⇒ worst case (holding 0, the peak rate). | |
now | number? | Unix-seconds override for “now”. Defaults to the current clock; pass a fixed value for deterministic previews/tests. | |
markPrice | string? | Pre-fetched mark price (decimal). Omit to fetch via the price service. | |
feeRates | FeeForUser? | Pre-fetched on-chain fee rates. Omit to fetch via getFeeForUser. | |
solverInfo | EnigmaSolverInfo? | Pre-fetched solver static fees. Lowcap/Enigma only — ignored elsewhere. Omit to fetch via getSolverInfo (fail-soft). | |
chainId | number? | Optional chain override. | |
solverId | string? | Optional solver override. |
Returns
GetInstantCloseFeesReturnType — EnigmaInstantCloseFees | RasaInstantCloseFees, discriminated on kind. Every kind carries platformCloseFee, notional (the close notional quantity × markPrice the rates were applied to), and totalFee; narrow on kind to reach the lowcap-only legs:
| Field | Kind | Notes |
|---|---|---|
platformCloseFee | both | Platform close leg, charged on-chain after PnL settlement. |
notional | both | Close notional the rates were applied to (decimal string). |
totalFee | both | Sum of every close leg this kind charges. |
closeSolverFee | enigma | Solver close leg at the priced holding time, capped on-chain by the signed closeRateCap. |
closeSolverFeeRate | enigma | The rate the leg was priced at (decimal fraction string) — for rate displays. |
holdingSeconds | enigma | Holding time the schedule was evaluated at, in seconds. |
staticSolverFeeClose | enigma | Flat USD static close leg from /info — size-independent, "0" when the solver omits it. |
Errors
INVALID_TRADE_PARAMETERS—quantityis not a positive decimal, or the resolved mark price is zero/NaN.RESOLVE_MARKET_NOT_FOUND— solver has no market for that id.RESOLVE_MARK_PRICE_NOT_FOUND— price service returned no tick for the symbol.
getInstantCloseFeesQueryOptions / getInstantCloseFeesQueryKey ship alongside for TanStack Query consumers — the query is disabled until a non-empty quantity exists, and the holding time is re-stamped on every fetch, so a refetch re-prices the decaying solver leg as the position ages. The React layer wraps them as useInstantCloseFees.
Query / Mutation options
instantCloseMutationOptions(config)— TanStack mutation bag for the primitive.instantCloseAutoMutationOptions(config)— TanStack mutation bag for the wrapper.instantCloseBulkMutationOptions(config)/instantCloseBulkAutoMutationOptions(config)— bulk variants.getInstantClosesQueryOptions(config, options)— TanStack query bag for the pending list.getInstantCloseFeesQueryOptions(config, options)— TanStack query bag for the close-fee preview.
Related
- Instant Open — the open-side analog with the same split.
- Unified Quotes —
PendingInstantClosefeeds the reconciler; close-side lifecycle transitions live inapplyNotificationToQuotes. - Notifications — close notifications drive
CLOSE_PRICE_FILLED/WRITE_ONCHAIN_CLOSEtransitions.