Publish a Module
Signed handler bundles that add new providers or capabilities. Free by default; flip one flag to sell them with offline-verified licenses.
tools/list visibility, local-UI composition, licensing, review process.Module anatomy
A module is three files under ~/.railcall/station/modules/<slug>/. The loader verifies the Ed25519 signature before importing the handler — a bad signature refuses to load.
~/.railcall/station/modules/your-handle/example/
├── module.json # manifest — slug, commands, license flag
├── handlers/
│ └── handler.py # your handler code
└── module.sig # Ed25519 signature over the bundlemodule.json
The manifest declares the commands your handler exposes and whether the module is gated behind a license. Keep it minimal.
{
"slug": "your-handle/example",
"version": "0.1.0",
"description": "Short one-liner shown on the listing.",
"commands": [
{ "name": "do_thing", "description": "What this command does." }
],
"license_required": false
}Signing & publishing
railcall market publish signs the bundle with your publisher key and uploads it. Users install the same way they install workflows.
railcall market claim your-handle/example
railcall market publish
# ↳ writes module.sig
# ↳ uploads to railcall-marketplace-lggm.onrender.comMaking it paid
Flip license_required: true in module.json. The loader then refuses to import the handler unless a valid entitlement is present for this install.
{
"slug": "your-handle/example",
"version": "0.1.0",
"license_required": true
}Licenses are minted by the RailCall license service (railcall-license.onrender.com), bound to the buyer's install pubkey, and verified offline against the pinned issuer public key. Seven-day grace past expiry so a flaky network doesn't break production.
Live example: sami666/salesforce
Thirteen commands across leads, contacts, accounts, opportunities, and cases. OAuth 2.0 refresh flow via railcall connect salesforce. $199/mo. That's the reference shape for a paid connector.
Live example: sami666/hubspot
Two commands (create_contact, log_note), Bearer token auth, free. That's the reference shape for a free connector.
What buyers do
railcall market install your-handle/example
railcall license activate your-handle/example # only if license_required
railcall license verify # offlinemodule.sig. The publish command re-signs on every upload; a stale or forged signature bounces at load time and users see the failure.Credentials + sandbox (v0.56+)
credential_spec works with catalogue providers. If your module declares a provider name that exists in the built-in catalogue (salesforce, hubspot, zoho… ~110 ids), your custom fields render in Studio's Configure form auto-namespaced as your-slug::provider— your handler's vault_get("provider") routes there transparently. No handler changes.
Network allowlists actually enforce — and actually work. Declare requires.network: ["api.example.com"] and standard requests/urllib calls to allowed hosts pass (the pre-v0.56 DNS-resolution false-block is fixed), while undeclared hosts and raw-IP dials refuse loudly. Auth reads must come from the vault — os.environ credential reads fail review.
The `requires` block — full reference
Without a top-level requires block your module runs with Studio's full privileges and shows SANDBOX · UNRESTRICTED in the Modules tab. Declaring one is opt-in, never breaking, and flips the badge to your declared capabilities — buyers see exactly what your handler may touch.
{
"id": "your-handle/linear-guard",
"requires": {
"network": ["api.linear.app", "*.optional-wildcards.com"],
"subprocess": false,
"filesystem_writes": []
},
"commands": [ ... ]
}- network — hostname allowlist for outbound connections (wildcards ok). Anything not listed raises
SandboxViolationwith the offending host in the message. Empty list = no network. - subprocess — boolean.
falsemakes anysubprocess.*call raise. - filesystem_writes — glob list of writable paths (e.g.
["/tmp/**"]). Empty list = no writes. Reads are not restricted — only writes.
The top-level requires (shown above) is the sandbox capability block. The per-command commands[].requires is a list of credential names that command needs — a completely different meaning. Also distinct: allowed_destinations governs the egress-broker checks, not handler socket calls. Declare both when they apply.
Changing it requires a re-sign. requires lives in module.json, and the bundle signature covers the canonical manifest — an edited manifest without a re-sign fails the loader's signature check. So: edit → railcall market module sign → bump the version → railcall market publish. The station reads the block at module load.
Honest scope: this is language-level enforcement — designed to catch capability drift and AI-drafted mistakes loudly, not to contain a determined adversary. Publisher signatures + the trust allowlist remain the primary defense; OS-level sandboxing (sandbox-exec / systemd IPAddressDeny) layers on top where available.
Revenue
You keep 95% of your listing price — on one-time sales and on every subscription renewal.The buyer additionally pays a 10% assurance fee on top of your price (their own line item at checkout, effective 26 Aug 2026) — it funds the airlock itself: signature verification, provenance, blast-radius vetting and revocation on every module. Your economics never change because of it. Wallet-and-payout model — earnings accumulate in your marketplace wallet and you withdraw when you're ready. Example: a $100 module → buyer pays $110, you keep $95, RailCall's $15 covers the 5% take plus the buyer's assurance fee.