START

First hour

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

Start from a Foundry project that already compiles with forge build. A project without forge-std dies on script/ before Interlude says anything useful. No project yet? npx create-interlude-app my-app writes a Foundry contract and a Next.js page already wired to Interlude, with the commands in its README.

1. Install the CLI in Foundry, not the SDK

terminal
npm init -y   # a fresh forge init has no package.json
npm i -D @interludelayer-sdk/cli@^0.2.3
npx @interludelayer-sdk/cli init

Without a package.json of its own, npm i installs into the nearest parent folder that has one, and the CLI lands outside the project.

Install @interludelayer-sdk/cli and @interludelayer-sdk/sdk 0.2.3, the release on npm (0.2.x is needed). The 0.1.x releases are older: cli 0.1.6 silently ignores --owner and --out (so the contract it ships stays Interlude’s) and has no abi or status. A ^0.1 range never resolves to 0.2.

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.28;

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. The vendored contracts use transient storage, so the project needs solc 0.8.28 or later and evm_version cancun or later. init warns if foundry.toml pins something older.

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

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 --owner 0xYourWallet --out ../web/.env.local
npx @interludelayer-sdk/cli abi --out ../web/lib/your-app-abi.ts

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 with the constructor args and [[setup]] calls from interlude.toml (it refuses to guess an argument you did not give), and prints app plus node. --out merges them into your frontend’s .env.local as NEXT_PUBLIC_INTERLUDE_APP / _NODE / _BASE_RPC; abi writes a typed as const ABI there. A 502 for a few minutes is the image building. Do not point the SDK at https://rpc.interludelayer.xyz.

With --owner, control offers you the contract once the delegation is open. Run the acceptOwnership() command ship prints and it is yours. That command is a Monad transaction from your wallet, so the wallet needs a little testnet MON: the Monad faucet has it. Without --owner, Interlude’s key stays the owner. npx @interludelayer-sdk/cli status 0xYourApp shows the owner, the pending owner, the epoch and the node’s health. A per-key surface is refused: the hosted node serves the whole contract.

5. Frontend, a different folder

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

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"),
  }),
});

Then, on a click in the browser: check the node, build the wallet, open the session, call.

app/play.ts
import "viem/window"; // types window.ethereum
import { createWalletClient, custom } from "viem";
import { monadTestnet } from "viem/chains";
import { interlude } from "@/lib/interlude";

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

  // The browser wallet, with its account. With wagmi: useWalletClient().data.
  const [account] = await createWalletClient({ transport: custom(window.ethereum!) }).requestAddresses();
  const wallet = createWalletClient({ account, chain: monadTestnet, transport: custom(window.ethereum!) });

  // ensureChain: the wallet moves to Monad testnet (and adds it if needed) before it signs.
  const session = await interlude.openSession({ wallet, scope: ["play"], ensureChain: true });
  await session.send("play");
  return interlude.read("currentScore");
}

The session needs a wallet client that carries an account, or openSession throws SessionUnusableError. Without ensureChain, a wallet on another network throws WrongChainError before it is asked to sign. The user needs no MON: the grant is a signature and every call after it is gasless.

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. Its node listens on the port under [node] in interlude.toml, 8555 as init writes it.

Already have a contract? Integrate an existing contract takes one with an OpenZeppelin owner, a parent contract, a struct and an ERC-20 through the same steps.