Snip 36 logo

Snip 36

Organization
keep-starknet-strange
snip-36

SNIP-36 virtual block proving on Starknet. Trigger on "virtual block", "SNIP-36", "off-chain proof", "anonymous vote", "heavy computation off-chain", "prove a transaction". Covers Cairo virtual contract, proof server, starknet.js integration, and on-chain verification.

Overview

Publisherkeep-starknet-strange
Repositorystarknet-agentic
Skill namesnip-36
Stars
81
Forks
26
Bundled files
2
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 2 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by keep-starknet-strange on GitHub. Read the source before you install it.

Installation

Install the Snip 36 AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/keep-starknet-strange/starknet-agentic/tree/main/skills/snip-36
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/keep-starknet-strange/starknet-agentic.git /tmp/starknet-agentic
mkdir -p .claude/skills
cp -r /tmp/starknet-agentic/skills/snip-36 .claude/skills/snip-36
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Snip 36 in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Snip 36 on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Snip 36 is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

SNIP-36

Overview

SNIP-36 allows executing a single INVOKE_TXN_V3 off-chain against a reference Starknet block's state, then submitting a stwo-cairo proof on-chain. The proof extends the standard v3 transaction hash by appending a proof_facts_hash. Contracts verify this via get_execution_info_v3_syscall.

Core value: Run arbitrary Cairo logic off-chain (heavy computation, privacy checks, game outcomes, attribute proofs) and commit only the verified result on-chain — without revealing private inputs.

Spec: https://community.starknet.io/t/snip-36-in-protocol-proof-verification/116123

Reference implementation: https://github.com/starknet-innovation/snip-36-prover-backend


Quick Start

  1. Review the operator checklist.
  2. Add a virtual create_proof function that emits one L2->L1 message.
  3. Prove an unsigned virtual tx with snip36 prove virtual-os.
  4. Submit verify_result with { proof, proofFacts } and the decoded message.

When to Use

  • The user asks for SNIP-36, virtual block proving, off-chain Starknet proof generation, or proof-backed on-chain verification.
  • The workflow needs heavy Cairo computation, privacy-preserving inputs, anonymous voting, secret whitelist checks, or replay-safe nullifier patterns.
  • The implementation needs a Cairo virtual function, proof server, starknet.js signing flow, and on-chain verify_result contract pattern.

When NOT to Use

  • The user needs a normal Starknet transaction that should be broadcast and fee-estimated through standard RPC.
  • The proof cannot run on a backend with native binaries, disk, and about 18 GB RAM.
  • The security model requires SNOS-native verification instead of Phase 1 sequencer-side proof verification.

Use Cases

  • Heavy computation: prove a large hash or algorithm result, then store only the verified output.
  • Private attributes: prove age, whitelist membership, or voting weight with a nullifier and public boolean/result.
  • Provable games: commit coin flips, seeds, bets, and outcomes for on-chain settlement.
  • ZKThread or shard transitions: prove { old_root, new_root, ... } before updating L2 state.

Generic 3-Phase Pattern

text
PHASE 1 — CREATE (off-chain build)
  Build a signed INVOKE_TXN_V3 that calls the virtual function.
  Never broadcast. Includes public_input + private_input in calldata.

PHASE 2 — PROVE (proof server)
  POST { blockNumber, tx } → snip36 prove virtual-os
  Returns { proof, proofFacts, l2ToL1Messages }
  Duration: ~40-50s, ~18 GB RAM

PHASE 3 — VERIFY (on-chain)
  execute(verify_call, { proof, proofFacts })
  Contract reads proof_facts, recomputes message hash, applies state change.

Part 1 — Cairo Contract

Scarb.toml requirements

toml
[[target.starknet-contract]]
allowed-libfuncs-list.name = "all"   # required for get_execution_info_v3_syscall

Virtual function pattern

cairo
// Called VIRTUALLY (by proof server). Never call directly on-chain.
// public_input  → included in L2→L1 message (visible to verifier)
// private_input → used in computation but NEVER revealed on-chain
fn create_proof(
    ref self: ContractState,
    public_input: PublicInput,
    private_input: PrivateInput,
) {
    // 1. Compute result using both inputs
    let result = heavy_computation(public_input, private_input);

    // 2. Commit result as L2→L1 message — this becomes the proof output
    let mut payload: Array<felt252> = array![];
    // serialize fields the verifier will need:
    payload.append(public_input.field1);
    payload.append(result);
    send_message_to_l1_syscall(
        to_address: 0,   // unused for SNIP-36 (no L1 delivery)
        payload: payload.span()
    ).unwrap();
}

On-chain verify function pattern

cairo
// Called ON-CHAIN with proof attached via { proof, proofFacts }.
fn verify_result(
    ref self: ContractState,
    public_message: PublicMessage,  // decoded from l2ToL1Messages[0].payload
) {
    // 1. Read proof_facts committed by SNIP-36
    let info = starknet::syscalls::get_execution_info_v3_syscall()
        .unwrap_syscall().unbox();
    let proof_facts = info.tx_info.unbox().proof_facts;

    // 2. Recompute message hash from the submitted public_message
    let message_hash = compute_message_hash(get_contract_address(), @public_message);

    // 3. Assert proof integrity: proof_facts[8] must equal our hash
    assert(*proof_facts[8] == message_hash, 'Proof message mismatch');

    // 4. Apply state change (nullifier, store result, transfer funds, etc.)
    // ...
}

proof_facts layout

text
index: [0,  1,  2,                   3,  4,            5,          6,              7,          8,             ...]
value: [0,  0,  virtual_OS_prog_hash, 0,  block_number, block_hash, OS_config_hash, n_messages, msg_hash_0,    msg_hash_1, ...]
  • [7] = number of L2→L1 messages emitted (usually 1)
  • [8] = Poseidon hash of first message (checked against recomputed hash)

Message hash computation (Cairo)

cairo
fn compute_message_hash(contract_addr: ContractAddress, msg: @PublicMessage) -> felt252 {
    let mut payload: Array<felt252> = array![];
    (*msg).serialize(ref payload);
    let mut data: Array<felt252> = array![
        contract_addr.into(),
        0_felt252,
        payload.len().into(),
    ];
    for f in payload.span() { data.append(*f); }
    poseidon_hash_span(data.span())
}

Nullifier pattern (for replay protection)

cairo
const NULLIFIER_DOMAIN: felt252 = 'my_app_nullifier_v1';

fn compute_nullifier(unique_id: felt252, secret: Span<felt252>) -> felt252 {
    let secret_hash = poseidon_hash_span(secret);
    poseidon_hash_span(array![NULLIFIER_DOMAIN, unique_id, secret_hash].span())
}

TypeScript equivalent (must match exactly):

typescript
const NULLIFIER_DOMAIN = shortString.encodeShortString("my_app_nullifier_v1");

function computeNullifier(uniqueId: string, secret: string[]): string {
    const secretHash = hash.computePoseidonHashOnElements(secret);
    return hash.computePoseidonHashOnElements([NULLIFIER_DOMAIN, uniqueId, secretHash]);
}

Part 2 — Proof Server (Node.js + Rust CLI)

Setup

bash
git clone https://github.com/starknet-innovation/snip-36-prover-backend
cd snip-36-prover-backend
cargo build --release -p snip36-cli
./target/release/snip36 setup

cp .env.example .env
# STARKNET_RPC_URL=...      (v0.8+ JSON-RPC, e.g. Alchemy)
# STARKNET_ACCOUNT_ADDRESS=...
# STARKNET_PRIVATE_KEY=...
# STARKNET_GATEWAY_URL=https://alpha-sepolia.starknet.io

Express server

typescript
// src/index.ts
import express from "express";
import { ensureBuilt } from "./build";
import { proveRouter } from "./prove";

ensureBuilt();
const app = express();
app.use(express.json());
app.use(proveRouter);
app.listen(Number(process.env.PORT ?? 3030));

POST /prove — SSE streaming endpoint

typescript
// src/prove.ts
proveRouter.post("/prove", (req, res) => {
    const { blockNumber, tx } = req.body ?? {};
    if (!Number.isInteger(blockNumber) || blockNumber < 0 || typeof tx !== "object" || tx == null) {
        res.status(400).json({ code: "SNIP36_INVALID_REQUEST", message: "blockNumber or tx is invalid" });
        return;
    }

    const txJsonPath = `./tmp/tx-${Date.now()}.json`;
    const outputBase = `./output/prove-${Date.now()}`;
    const proofPath = `${outputBase}.proof`;
    const proofFactsPath = `${outputBase}.proof_facts`;
    const rawMessagesPath = `${outputBase}.raw_messages.json`;
    const cleanupPaths = [txJsonPath, proofPath, proofFactsPath, rawMessagesPath];
    const args = [
        "prove", "virtual-os",
        "--block-number", String(blockNumber),
        "--tx-json", txJsonPath,
        "--rpc-url", process.env.STARKNET_RPC_URL!,
        "--output", proofPath,
    ];

    res.setHeader("Content-Type", "text/event-stream");
    res.flushHeaders();

    let child: ReturnType<typeof spawn> | undefined;
    let timeout: NodeJS.Timeout | undefined;
    let settled = false;
    let timedOut = false;
    const errorDetails = (error: unknown) => error instanceof Error ? error.message : String(error);
    const send = (event: "log" | "done" | "error", data: object) =>
        !settled && res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);

    const cleanup = () => {
        for (const p of cleanupPaths) {
            try {
                if (fs.existsSync(p)) fs.unlinkSync(p);
            } catch (error) {
                send("log", { stream: "cleanup", line: `failed to remove ${p}: ${errorDetails(error)}` });
            }
        }
    };

    const finish = () => {
        if (settled) return;
        if (timeout) clearTimeout(timeout);
        cleanup();
        settled = true;
        res.end();
    };

    try {
        fs.mkdirSync("./tmp", { recursive: true });
        fs.mkdirSync("./output", { recursive: true });
        fs.writeFileSync(txJsonPath, JSON.stringify(tx));
        child = spawn(BINARY, args, { cwd: REPO_CWD, env: process.env });
    } catch (error) {
        send("error", { code: "SNIP36_PROVER_START_FAILED", message: "failed to start snip36", details: errorDetails(error) });
        finish();
        return;
    }

    timeout = setTimeout(() => {
        timedOut = true;
        send("error", { code: "SNIP36_PROVER_TIMEOUT", message: "snip36 timed out", details: "terminated after 10 minutes" });
        child?.kill("SIGTERM");
    }, 10 * 60 * 1000);

    child.on("error", error => {
        send("error", { code: "SNIP36_PROVER_START_FAILED", message: "snip36 process failed", details: errorDetails(error) });
        finish();
    });
    child.stdout.on("data", c => send("log", { stream: "stdout", line: c.toString() }));
    child.stderr.on("data", c => send("log", { stream: "stderr", line: c.toString() }));

    child.on("close", code => {
        if (settled) return;
        try {
            if (timedOut) return;
            if (code !== 0) {
                send("error", { code: "SNIP36_PROVER_EXIT_NON_ZERO", message: "snip36 exited non-zero", details: `exit code ${code}` });
                return;
            }
            if (!fs.existsSync(proofPath) || !fs.existsSync(proofFactsPath)) {
                throw new Error("missing proof or proof_facts artifact");
            }
            const proof = fs.readFileSync(proofPath, "ascii").trim();
            const proofFacts = JSON.parse(fs.readFileSync(proofFactsPath, "ascii"));
            const l2ToL1Messages = fs.existsSync(rawMessagesPath)
                ? JSON.parse(fs.readFileSync(rawMessagesPath, "ascii")).l2_to_l1_messages
                : undefined;
            send("done", { proof, proofFacts, ...(l2ToL1Messages && { l2ToL1Messages }) });
        } catch (error) {
            send("error", { code: "SNIP36_ARTIFACT_READ_FAILED", message: "failed to read proof artifacts", details: errorDetails(error) });
        } finally {
            finish();
        }
    });
});

Output artifacts and env

The CLI emits *.proof (base64 stwo proof), *.proof_facts (JSON hex felt252s), and optional *.raw_messages.json with l2_to_l1_messages.

Required env: STARKNET_RPC_URL, STARKNET_ACCOUNT_ADDRESS, STARKNET_PRIVATE_KEY, STARKNET_GATEWAY_URL, STARKNET_CHAIN_ID, and PORT.

Part 3 — Starknet.js Integration

SSE client

typescript
// requestProof.ts
type ProveResult = { proof: string; proofFacts: BigNumberish[]; l2ToL1Messages?: { payload: BigNumberish[] }[] };

export async function requestProof(blockNumber: number, tx: INVOKE_TXN_V3): Promise<ProveResult> {
    const res = await fetch(`${process.env.PROOF_SERVER_URL ?? "http://localhost:3030"}/prove`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ blockNumber, tx }),
    });

    const reader = res.body!.getReader();
    const dec = new TextDecoder();
    let buf = "";
    let result: ProveResult | undefined;

    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        buf += dec.decode(value, { stream: true });
        const parts = buf.split("\n\n");
        buf = parts.pop() ?? "";
        for (const msg of parts) {
            const event = msg.match(/^event: (\w+)/)?.[1];
            const data = JSON.parse(msg.match(/^data: (.+)$/m)?.[1] ?? "null");
            if (event === "done") result = data;
            if (event === "error") throw Object.assign(new Error(data.message), { code: data.code, details: data.details });
        }
    }
    if (!result) throw new Error("No proof result");
    return result;
}

Virtual transaction build + proof orchestration

typescript
// server-side only: private inputs and signing keys must never reach the browser.
async function proveAndVerify(publicInput: unknown, privateInput: unknown): Promise<string> {
    const provider = new RpcProvider({ nodeUrl: process.env.RPC_URL! });
    const account = new Account({
        provider,
        address: process.env.ACCOUNT_ADDRESS!,
        signer: process.env.PRIVATE_KEY!,
    });
    const contract = new Contract({ abi, address: CONTRACT_ADDRESS, providerOrAccount: account });

    // 1. Build virtual call (never broadcast)
    const call = contract.populate("create_proof", { public_input: publicInput, private_input: privateInput });

    // 2. Must set resourceBounds manually; fee estimation would expose private calldata.
    const prices = await provider.getGasPrices();
    const M = 2n;
    const resourceBounds = {
        l2_gas:     { max_amount: 0x279fc0n * M, max_price_per_unit: prices.l2GasPrice * M },
        l1_gas:     { max_amount: 0xbd2an * M,   max_price_per_unit: prices.l1GasPrice * M },
        l1_data_gas:{ max_amount: 0xc0n * M,     max_price_per_unit: prices.l1DataGasPrice * M },
    };

    // 3. Requires the proof-enabled starknet.js fork, not standard starknet.js.
    const tx: INVOKE_TXN_V3 = await account.getSignedTransaction(call, { resourceBounds });
    const blockNumber = await provider.getBlockNumber();
    const proofResult = await requestProof(blockNumber, tx);

    const cd = new CallData(abi);
    const publicMessage = cd.decodeParameters(
        "my_contract::PublicMessage",
        proofResult.l2ToL1Messages![0].payload as string[]
    );

    const verifyCall = contract.populate("verify_result", { public_message: publicMessage });
    const { transaction_hash } = await account.execute(verifyCall, {
        proof: proofResult.proof,
        proofFacts: proofResult.proofFacts,
    } as any);

    return transaction_hash;
}

SNIP-36 transaction hash extension

text
Standard v3: poseidon(INVOKE, version, sender, tip_rb_hash, paymaster_hash,
                      chain_id, nonce, da_mode, acct_deploy_hash, calldata_hash)

SNIP-36:     poseidon(INVOKE, version, sender, tip_rb_hash, paymaster_hash,
                      chain_id, nonce, da_mode, acct_deploy_hash, calldata_hash,
                      proof_facts_hash)    ← appended only when proof_facts present

account.execute(call, { proof, proofFacts }) handles the hash extension automatically in starknet.js.

Frontend boundaries

  • Browser: collect public inputs, optionally request a wallet signature, and poll for the final transaction hash.
  • Server: keep RPC_URL, ACCOUNT_ADDRESS, PRIVATE_KEY, PROOF_SERVER_URL, getSignedTransaction(), and /prove calls off the client.
  • RPC proxy: route browser RPC through a server endpoint if the provider key is sensitive.

Only expose NEXT_PUBLIC_CONTRACT_ADDRESS or other non-secret addresses to the browser.


Critical Pitfalls

PitfallFix
Fee estimation on virtual txEstimating fees online would send the full calldata (including private inputs) to the RPC node — exposing secrets. Set resourceBounds manually at 2× current gas prices instead
Proof generation in browserThe proving backend requires ~18 GB RAM and a native Rust binary — impossible in a browser. The proof server call must go through a backend. On-chain submission can be done from the browser (wallet) if the RPC key is not sensitive, or from the backend to hide a dedicated account private key
Missing allowed-libfuncs-list.name = "all"get_execution_info_v3_syscall requires it
Proof server resources~18 GB RAM, 40-50s per proof, ~10 GB disk for deps
L2→L1 to_address for SNIP-36Set to 0 or any felt — no actual L1 message is sent
Nullifier domain mismatch between Cairo and TSMust use identical domain string and identical hash chain
Wrong blockNumber sent to proof serverUse provider.getBlockNumber() right before getSignedTransaction
Re-using a nullifierContract must check and revert; compute nullifier locally first to fail fast
Security caveat (Phase 1)Proofs are verified by sequencer only, not by SNOS — degraded security vs native Starknet
Proof pricing130 L2gas/byte (125 propagation + 5 storage) + 10M L2gas base overhead

Error Codes & Recovery

CodeNameSource / symbolRecovery
SNIP36_INVALID_REQUESTinvalid_requestHTTP /prove before CLI spawnValidate inputs before calling /prove; blockNumber must be a non-negative integer and tx must be a complete INVOKE_TXN_V3 object
SNIP36_PROVER_START_FAILEDprover_start_failedHTTP /prove, spawn(BINARY, args) or temp file setupConfirm the snip36 binary path, REPO_CWD, ./tmp, and ./output permissions; inspect proof server logs for the included details field
SNIP36_PROVER_TIMEOUTproof_generation_timeoutHTTP /prove, long-running snip36 prove virtual-osRetry on a dedicated backend; verify the host has ~18 GB RAM, available CPU, and no stuck prior prover process
SNIP36_PROVER_EXIT_NON_ZEROproof_generation_failedCLI flow: snip36 prove virtual-osCheck stderr logs, STARKNET_RPC_URL, STARKNET_CHAIN_ID, block availability, and whether the virtual transaction uses unsupported calldata or libfuncs
SNIP36_ARTIFACT_READ_FAILEDartifact_read_failedHTTP /prove reading .proof, .proof_facts, or .raw_messages.jsonCheck disk space and output permissions; verify the CLI wrote both proofPath and proofFactsPath before the server cleaned temporary files
SNIP36_FEE_ESTIMATION_SENSITIVEfee_estimation_sensitiveJS getSignedTransaction orchestrationDo not call fee estimation for a virtual transaction; set resourceBounds manually from provider.getGasPrices() before signing
SNIP36_STALE_BLOCK_NUMBERstale_block_numberJS provider.getBlockNumber() and HTTP /proveFetch provider.getBlockNumber() immediately before getSignedTransaction() and retry with a recent finalized block
SNIP36_PROOF_FACTS_MISSINGproof_facts_missingCairo verify_result, get_execution_info_v3_syscall()Ensure the on-chain account.execute(verifyCall, { proof, proofFacts }) includes both fields and uses the starknet.js proof hash extension
SNIP36_MESSAGE_HASH_MISMATCHproof_message_mismatchCairo verify_result, proof_facts[8] checkCompare the create_proof L2->L1 payload with the public_message passed to verify_result; check serialization order and compute_message_hash inputs
SNIP36_NULLIFIER_MISMATCHnullifier_mismatchCairo compute_nullifier, TS computeNullifierVerify the domain string, short-string encoding, field order, and Poseidon hash chain match exactly across Cairo and TypeScript
SNIP36_REUSED_NULLIFIERreused_nullifierCairo verify_result state update, optional read_result()Abort submission, call read_result() or the nullifier storage view, and surface a replay/conflict error to the user
SNIP36_RPC_CHAIN_MISMATCHrpc_chain_mismatchCLI/HTTP env: STARKNET_RPC_URL, STARKNET_GATEWAY_URL, STARKNET_CHAIN_IDAlign RPC, gateway, and account network; rebuild the virtual transaction after correcting environment variables

Quick Reference

text
Contract: create_proof(public, private) -> emit L2->L1 message
Contract: verify_result(public_msg) -> check proof_facts[8] and apply state
Optional: read_result() -> query stored result/nullifier
Facts:    proof_facts[7] = n_messages; proof_facts[8] = poseidon(contract, 0, len, payload)
CLI:      snip36 prove virtual-os --block-number N --tx-json tx.json --rpc-url URL --output out.proof
HTTP:     POST /prove { blockNumber, tx } -> SSE log* then done|error
JS:       getSignedTransaction(call, { resourceBounds }) then execute(verifyCall, { proof, proofFacts })

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Snip 36 AI skill do?

SNIP-36 virtual block proving on Starknet. Trigger on "virtual block", "SNIP-36", "off-chain proof", "anonymous vote", "heavy computation off-chain", "prove a transaction". Covers Cairo virtual contract, proof server, starknet.js integration, and on-chain verification.

Why use Snip 36 on TypingMind?

Because you install it once and use it with any model. Snip 36 is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Snip 36 in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/keep-starknet-strange/starknet-agentic/tree/main/skills/snip-36. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Snip 36?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Snip 36?

As many as you like. As long as a model supports skills, you can use Snip 36 with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Snip 36 AI skill free?

Yes. It is published on GitHub by keep-starknet-strange under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇