ON MONAD

Delegation

ship opens it. undelegate ends it; releaseStake unlocks the app and frees the bond after the challenge window.

Until you delegate, the engine has nothing to write. The contract is a normal Monad contract. Delegation is the one on-chain transaction that opens the fast path. undelegate is the one that closes it.

scripts/go-live.ts
import { parseEther } from "viem";

// ship already sent this. Again only to re-open after undelegate and releaseStake,
// forwarding the validator's delegationFee: 0.01 MON on the live terms.
await interlude.delegateAll(ownerWallet, { value: parseEther("0.01") });

ship already sent delegateAll() and paid its fee. You call it yourself only to re-open the contract after undelegate and releaseStake, and you forward the validator’s delegationFee (0.01 MON on the live terms, DEPLOYMENTS.md), or the hub reverts FeeNotPaid. The owner wallet is the contract owner, not the player, and it needs that MON (faucet).

delegateKey(ownerWallet, keyOf(user)) puts one user’s partition on the engine and leaves everyone else on Monad. It needs a per-key surface, which only interlude dev serves: a hosted node serves the whole contract, so on a shipped app delegateKey reverts EmptyDelegation.

When the session is done

Call undelegate on that partition. The session ends and the engine stops committing, but the lock does not lift yet: the partition stays locked, and the validator’s stake reserved, through the challenge window (~1 hour on the deployed terms, longer if a dispute froze the session). A fraud found in that window can still be unwound before anyone withdraws against it, and a bad batch plus an immediate exit cannot walk away with the bond. Guarded writes on Monad keep reverting with DelegatedWritesDisabled until then.

Nothing clears it on a timer. After stakeUnlockAt, call releaseStake on the Hub (permissionless; anyone may send it). That call unlocks the app and frees the stake: the contract is ordinary Monad again, same address, with the settled state. Before that time it reverts with StakeStillLocked. The SDK does not wait out the window — use a keeper, or call it once the window has passed.

scripts/close.ts
await interlude.undelegate(ownerWallet); // the whole contract: what ship opened

// After the challenge window (stakeUnlockAt), from any wallet:
await interlude.releaseStake(anyWallet);

A per-key partition (interlude dev) closes the same way, with its key as the second argument: undelegate(ownerWallet, keyOf(user)), then releaseStake(anyWallet, keyOf(user)).

If the engine stops committing for longer than the validator’s maxBatchInterval plus a five-minute grace, or the session passes its expiry, anyone can forceClose it. Silence cannot freeze the app. An idle node that is alive commits an empty heartbeat batch, so a quiet session is not mistaken for a dead one. (The previous hub, where earlier apps still run, only honours seven days of silence; the live hub enforces both.) forceClose also enters the challenge window: the app stays locked until releaseStake.