Redeem and close a position
Exiting a position the runtime opened.
Closing is a first-class action, the mirror of minting. A plugin returns vault.redeemBinary or vault.redeemRange from decide(), the runtime materializes the tx, and the position shrinks or disappears from the vault's tracked keys.
Auto-fund only runs before a mint. There is no symmetric auto-withdraw after a redeem. If you want proceeds back in the vault, you own that step.
The actions
Two action kinds close a position, one per shape:
{ kind: "vault.redeemBinary", params: { marketId, strike, isUp, quantity } }
{ kind: "vault.redeemRange", params: { marketId, lowerStrike, higherStrike, quantity } }marketIdis the oracle ID, same as on the matching mint. Pair it with thestrike/isUp(binary) orlowerStrike/higherStrike(range) that identify the open key.quantityis raw contracts. It may be partial: redeem less than the open size and the key stays tracked with the residual. Redeem the full open quantity and the key is removed fromtrackedMarketKeys/trackedRangeKeys.
To find what's open, read ctx.vault.trackedMarketKeys and ctx.vault.trackedRangeKeys. Each MarketKey carries oracle_id, strike, is_up; each RangeKey carries oracle_id, lower_strike, higher_strike. Those are exactly the fields the redeem params need.
When it's valid
- Permission. The strategy needs the redeem bit. Check
ctx.vault.canRedeemBinary/ctx.vault.canRedeemRangebefore returning the action. Without it the runtime's executor rejects the action in a local preflight — before any tx is built or signed — so the tick is wasted but nothing reaches the chain (no signature, no gas). The vault enforces the same permission on-chain as a backstop, but the local check means you never get there. - Not frozen. A redeem will abort if the vault is paused.
ctx.vault.isFrozenfoldspausedandstrategyPausedinto one read. - The key must exist. You can only redeem against an open key. Size and target the redeem from the tracked-keys lists rather than from memory.
Settlement and proceeds
A redeem returns quote into the vault's custody chain, but the SDK's PM-custody convention is two-step in both directions:
pmDeposit→mint*opens.redeem*→pmWithdrawcloses.
Proceeds from a redeem land in the PredictManager. To bring them back to the vault's idle quote balance, append a vault.pmWithdraw action in the same batch. Skip it and the capital sits in the PM and falls out of NAV until you sweep it later.
return [
{ kind: "vault.redeemBinary", params: { marketId, strike, isUp, quantity } },
{ kind: "vault.pmWithdraw", params: { amount } },
];pmWithdraw also needs its permission bit (ctx.vault.canPmWithdraw).
Knowing what actually closed
Don't reconstruct closures from your own bookkeeping. The runtime hands you ctx.lifecycle.closures at the top of each decide() — a delta of every position that closed since the last tick, with no cursor to manage.
Each ClosureEvent distinguishes how it closed via kind:
"redeem"— yourredeemBinary/redeemRangelanded."liquidation"— the vault force-closed the position during an LP withdrawal. Not builder-initiated, but you still see it here, so a redeem you never issued won't surprise you.
It also carries quantityClosed, remainingQuantity (0n means fully closed), quotePayout, the digest, and ts. When the runtime observed the opening mints in an earlier tick, it adds entryUnitCostRaw, pnlRaw, and pnlBps (VWAP-based). Those PnL fields are absent for positions opened before the runner started, or commingled across plugins sharing one vault.
async decide(ctx) {
for (const c of ctx.lifecycle.closures) {
ctx.logger.info("position closed", {
kind: c.kind,
marketId: c.marketId,
quantityClosed: c.quantityClosed.toString(),
remaining: c.remainingQuantity.toString(),
pnlBps: c.pnlBps ?? null,
});
}
// ... decide whether to re-open, rotate, or stand down
return [{ kind: "noop", reason: "observed closures, nothing to do" }];
}The closure feed is empty on most ticks, on the first tick, and after a restart (it's a delta, not a backfill).
One caveat on the SDK surface
The redeem action is fully wired through the runtime today. The SDK's Vault class does not yet expose a standalone redeemBinary / redeemRange tx-builder method — those are on the roadmap alongside the planned Strategy class. Inside a plugin you never need them: you return the action and the runtime builds the call. You'd only reach for a hand-rolled tx.moveCall if you were redeeming outside the runtime, which is not the path this ladder covers.
Next: the Lifecycle hooks page shows how to react to these closures in onExecuted instead of polling them in decide().