Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.adipredictstreet.com/llms.txt

Use this file to discover all available pages before exploring further.

When you request a vault.splitPosition or vault.mergePositions co-signature from the backend, the platform places an off-chain lock on your vault’s balance for the duration of the signed deadline:
  • Split — locks USDC (available -> locked) so it can’t be spent twice while the chain tx is in-flight.
  • Merge — locks per-outcome ERC-1155 shares so a SELL order can’t consume the same shares the merge plans to burn.
In the happy path your wallet submits the on-chain tx, the chain-event router watches the corresponding PositionSplit / PositionsMerged event, and the lock is drained (consumed by chain) with the collateral side credited symmetrically. You never see the lock.

When recovery is needed

Three failure shapes leave the lock stuck because the chain side never moved:
  1. You never broadcast — you got the backend signature back but never submitted the tx.
  2. The tx was dropped — broadcast but the mempool evicted it before mining (low gas, replacement, etc.).
  3. The tx reverted — landed in a block but failed the contract check; no PositionSplit / PositionsMerged event was emitted.
In any of these the off-chain signed_ops row hits the EXPIRED state by wall-clock once deadline elapses, but the balance lock remains until either:
  • The backend’s automatic sweeper job tops it up (default behaviour), or
  • You call the user-driven recovery endpoint described below (used when the sweeper is intentionally disabled in observation-only mode for a deploy window).

The recovery endpoint

POST /api/dual-signed-ops/{opId}/release-expired Supported op_kind: SPLIT and MERGE. CONVERT / REDEEM follow in subsequent releases. The endpoint runs a chain of safety gates before releasing the lock so a signed_ops row that did succeed on-chain can never be unlocked again (which would double-credit the vault):
  1. Ownership — caller’s X-User-Wallet must own the op’s vault_address (403 vault_owner_mismatch).
  2. Op kindSPLIT or MERGE only (400 op_kind_unsupported).
  3. Already terminal — already-refunded rows return an idempotent 200 with refundAppliedNow: false so retries are safe.
  4. Agenow - deadline ≥ 90 s (425 op_too_recent). The 90 s buffer matches the auto-sweeper’s safety window so a tx mined just after the off-chain deadline can never be double-counted.
  5. Chain-watcher liveness — refuses the call if chain-watcher’s own /health/chain-sync reports a lag past the local threshold (503 chain_watcher_lagging). Without this gate an un-indexed confirmed split would look like a no-execute and could trigger a double-credit.
  6. Chain-row absenceLEFT JOIN against the per-kind events table (chain_watcher.vault_position_split_events for SPLIT, chain_watcher.vault_positions_merged_events for MERGE) keyed by (vault, conditionId, amount). If a matching row exists the call returns 409 op_confirmed_on_chain with the chain event_key, tx_hash, and blockNumber in the error details so operators can re-drive the chain-event-router instead of unlocking duplicate funds.
On success the op transitions to EXPIRED with refunded=true and a single balance_events row appears with reason split_user_refund_verified (SPLIT) or merge_user_refund_verified (MERGE) — distinct from the sweeper’s *_expired_refund so audit queries can attribute the unlock to the user-driven path. See the full request / response shape in the API Reference.