START

First hour

One path: Foundry project, annotate, generate, ship, point the SDK at the URL it printed. Do these steps in order.

Do these in order, from a Foundry project that already compiles with forge build. A project without forge-std dies on script/ before Interlude says anything useful.

1. Install the CLI in Foundry, not the SDK

terminal
npm i -D @interludelayer-sdk/cli
npx @interludelayer-sdk/cli init

That first init prints a starter and exits 1. That is expected: nothing inherits Delegatable yet. It also wrote lib/interlude and the remapping. Always call the scoped binary. A bare interlude is not on PATH after npx.

2. Add the contract, pragma included

src/YourApp.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Delegatable} from "@interludelayer/contracts/Delegatable.sol";
import {IInterludeHub} from "@interludelayer/contracts/interfaces/IInterludeHub.sol";
import {Types} from "@interludelayer/contracts/interfaces/Types.sol";

contract YourApp is Delegatable {
    /// @custom:interlude global
    uint256 internal score;

    constructor(IInterludeHub hub_) Delegatable(hub_) {}

    function play() external whenNotDelegated(Types.GLOBAL) {
        score += 1;
    }

    function currentScore() external view returns (uint256) {
        return score;
    }
}

The comment is what the generator reads. The view is so the frontend can read the number. An internal score has no ABI getter.

What Types.GLOBAL is

Types.sol is the Hub’s shared structs. For the first hour you only need one constant: Types.GLOBAL, the partition for the whole contract. whenNotDelegated(Types.GLOBAL) is the lock. Without it, Monad still accepts a write the node also holds, and the next commit stalls. Per-user partitions are keyOf(user) later. You do not open the rest of that file to ship.

3. Generate the surface

terminal
npx @interludelayer-sdk/cli gen --contract YourApp

Writes YourAppInterludeSurface.sol beside the source. Same-directory Solidity is not auto-imported. Keep the Delegatable import. The constructor still names Delegatable(hub_). Dropping it does not compile.

src/YourApp.sol
import {Delegatable} from "@interludelayer/contracts/Delegatable.sol";
import {IInterludeHub} from "@interludelayer/contracts/interfaces/IInterludeHub.sol";
import {Types} from "@interludelayer/contracts/interfaces/Types.sol";
import {YourAppInterludeSurface} from "./YourAppInterludeSurface.sol";

contract YourApp is YourAppInterludeSurface {
    /// @custom:interlude global
    uint256 internal score;

    constructor(IInterludeHub hub_) Delegatable(hub_) {
        _registerInterludeSurface();
    }

    function play() external whenNotDelegated(Types.GLOBAL) {
        score += 1;
    }

    function currentScore() external view returns (uint256) {
        return score;
    }
}

4. Config, check, ship

terminal
npx @interludelayer-sdk/cli init --contract YourApp
npx @interludelayer-sdk/cli check
npx @interludelayer-sdk/cli ship

init writes interlude.toml. It succeeds even if you forgot to inherit the surface. check only compares layouts, it does not prove you registered. ship deploys on Monad testnet and prints app plus node. A 502 for a few minutes is the image building. Do not point the SDK at https://rpc.interludelayer.xyz.

5. Frontend, a different folder

terminal
npm i @interludelayer-sdk/sdk viem
lib/interlude.ts
import { createPublicClient, http } from "viem";
import { monadTestnet } from "viem/chains";
import { createInterludeClient } from "@interludelayer-sdk/sdk";
import { yourAppAbi } from "./your-app-abi";

export const interlude = createInterludeClient({
  app: "0xYOUR_APP",      // printed by ship
  abi: yourAppAbi,
  node: "https://YOUR_NODE", // printed by ship. Not the public demo.
  base: createPublicClient({
    chain: monadTestnet,
    transport: http("https://testnet-rpc.monad.xyz"),
  }),
});

const status = await interlude.status();
if (status.app.toLowerCase() !== interlude.app.toLowerCase()) {
  throw new Error("this node is serving a different contract");
}

const session = await interlude.openSession({ wallet, scope: ["play"] });
await session.send("play");
await interlude.read("currentScore");

Next: Point it at your app. The public 3D floor is a different pair, on The demo app.

interlude dev is the laptop loop. The published CLI does not ship the node binary. Local port in interlude.toml is 8555, not 8546.