Your first module — 30 minutes, zero to published

Walks you from `curl install.sh | bash` to a signed module live on the marketplace. Every command has expected output; copy-paste is safe. If you're building for the RailCall community contest, this is where you start.

What you'll build

A free module called your-handle/greeter with one command: greet. It takes a name input and returns {"message": "Hello, <name>!"}. Boring on purpose — the goal is to prove the whole loop works so you can substitute your real logic in step 6.

Prerequisites

  • Python 3.9+ (python3 --version)
  • curl or wget
  • ~30 minutes
  • An email address for the marketplace account

1. Install the RailCall CLI (30 seconds)

shell
curl -sSL https://railcall.ai/install.sh | bash

Installs to ~/.railcall/ with a launcher on your PATH. Every downloaded file is sha256-pinned against the installer — a wire tamper fails the pin gate before anything touches disk.

Verify:

shell
railcall version
# → RailCall CLI · station-v0.27 · <build hash>

2. Create your marketplace account (2 min)

Sign up at railcall.ai/marketplace/signup (email + password). Click the verification link in your inbox.

Log in from the CLI:

shell
railcall market login your@email.com
# → prompts for password
# → prints: logged in as your@email.com · session saved 0600

3. Mint a publisher keypair (1 min)

Every listing you publish is Ed25519-signed with your publisher key. The keypair is minted locally + never leaves your machine; the public half is registered on your account so verification works for anyone who downloads your module.

shell
railcall market publisher init your-handle
# → generates ~/.railcall/publisher_key.json (0600)
# → prints your public key (64 hex chars)

railcall market publisher register
# → registers the pubkey on your marketplace account
# → prints: registered publisher pubkey <first 16 chars>...
Publisher-trust allowlist (Phase 4a)

Registering the pubkey doesn't automatically let you publish paid modules — those require staff approval on the allowlist. Free modules work immediately. For the contest, publishing free is fine; paid comes after your first successful free module + a quick verification email from sami@railcall.ai.

4. Create the module scaffold (2 min)

Create the directory structure:

shell
mkdir -p ~/railcall-modules/greeter/handlers
cd ~/railcall-modules/greeter

module.json

The manifest declares which commands your handler exposes + auth pattern + whether the module gates on a license. Copy this verbatim:

json
{
  "slug": "your-handle/greeter",
  "version": "0.1.0",
  "description": "Says hello to a name you provide.",
  "auth": { "type": "none" },
  "commands": [
    {
      "name": "greet",
      "description": "Return a greeting for the given name.",
      "input_schema": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Who to greet." }
        },
        "required": ["name"]
      },
      "side_effects": "none"
    }
  ],
  "license_required": false
}

Save that as ~/railcall-modules/greeter/module.json.

handlers/handler.py

The handler implements every command declared in module.json. One top-level function per command, name matches the manifest.

python
"""Handlers for the your-handle/greeter module.

Every command declared in module.json must have a matching top-level
function here. The function's return value becomes the receipt payload.
"""


def greet(inputs: dict, context: dict) -> dict:
    """Return a friendly greeting.

    inputs: the JSON body validated against input_schema in module.json
    context: RailCall runtime info — install_pubkey, org_id (if any),
             workspace path, etc. Ignored here; useful for real modules
             that need to know where they're running.
    """
    name = inputs.get("name", "world")
    return {"message": f"Hello, {name}!"}

Save that as ~/railcall-modules/greeter/handlers/handler.py.

5. Test locally before publishing (2 min)

Install the module into your local Studio + run the command with real inputs. If it works here, it'll work for anyone who installs the published version.

shell
railcall module install --from-path ~/railcall-modules/greeter
# → installs into ~/.railcall/station/modules/your-handle/greeter/

railcall run your-handle/greeter.greet --name=Alice
# → {"message": "Hello, Alice!"}
# → signed receipt at ~/.railcall/receipts/greet-<ts>.json
What just happened

The airlock ran your handler, validated the input againstinput_schema, wrote the response, and emitted an Ed25519-signed receipt. Every buyer who installs your module gets the same behavior + receipt discipline — that's what they're paying for (or getting free, in your case).

6. Substitute your real logic

This is where you replace the greeter with what you actually want to build. Common patterns you'll want:

  • API call to an external service: add "auth": { "type": "api_key", "env_var": "MY_API_KEY" } to module.json; the CLI prompts the buyer to set the env var on install.
  • OAuth (Google, Salesforce, Slack, etc.): add "auth": { "type": "oauth2", "provider": "salesforce" }. See the auth patterns doc for provider setup.
  • Multiple commands: add more entries to the commands array + matching functions in handler.py.
  • Side effects (sending, writing, deleting): set "side_effects": "external" — RailCall's airlock forces preview → approve → execute for those commands, so buyers see the diff before you fire.

7. Publish to the marketplace (2 min + review)

The publish command signs the bundle + uploads. Rate-limited to 5 publishes per hour per account.

shell
cd ~/railcall-modules/greeter
railcall market publish .
# → signs bundle with your publisher key
# → uploads to railcall-marketplace-lggm.onrender.com
# → prints: published your-handle/greeter@0.1.0 · status: pending_review
Review step (fast — usually same day)

Every new listing lands in a pending_review queue before it appears on the public marketplace. This protects buyers from broken or misleading modules and keeps the catalog trustworthy.

Approval is fast — during the contest window we review submissions same-day (often within a couple hours). You'll see the listing on your dashboard immediately in PENDING_REVIEW state; once approved, it flips to ACTIVE and appears on the public browse.

What we check: (a) the module actually installs + runs, (b) the description matches what it does, (c) no obvious security issues (secrets logged, side_effects flag lying). Anything else is fair game — we're not judging your architecture, just protecting buyers.

Once approved, anyone can install it with:

shell
railcall market install your-handle/greeter
railcall run your-handle/greeter.greet --name=World

8. Verify a fresh buyer's install (3 min)

Best test: install it as a buyer would, from a directory that doesn't have your source. If it works here, it works for real buyers.

shell
# From a fresh directory + fresh session
cd /tmp
railcall market install your-handle/greeter
railcall run your-handle/greeter.greet --name="fresh install test"
# → {"message": "Hello, fresh install test!"}
# → signed receipt

Once your free module is stable + you've been approved on the publisher-trust allowlist (ping us in Discord with a link to your listing):

  1. Flip "license_required": true in module.json
  2. Set your price at marketplace/dashboard → one-time or monthly subscription
  3. railcall market publish . again — bump the version

RailCall takes 5% on one-time sales, 25% on subscription modules. Payouts to your wallet after a 5-day refund hold, then withdraw via Stripe Connect. Full terms: /legal/marketplace.

Next steps

A note on the airlock

Your handler doesn't call the airlock directly — it just returns data. The RailCall runtime wraps every command in the airlock automatically: input validation, preview generation, approval gate (for side-effect commands), execution, receipt signing. You get all of that with zero extra code. That's the whole point of publishing to RailCall vs shipping a bare Python package.