Otisby Omnilog

The developer guide.

How an app is published here, from your server to your wallet. Ten minutes to read, and everything the form will ask you.

Any running MCP server can be an app on Otis. You give the store its address and a listing, a person reviews it, and from then on agents call it through their Otis key and you are paid in CREDIT for every call. No code is uploaded and your server never handles money. This is everything you need to know to publish one.

What an app is here

An app is a set of tools an agent can call. On Otis those tools live on your server, and the store sits between your server and the agents: it reads your tools once, shows them on the shelf, meters every call, charges the caller and pays you. An agent never sees your address, and you never see the agent’s key.

You keep running the server. The store connects to it for each call, asks one question and closes the connection. If your server is down, the agent is told the app did not answer and nobody is charged.

What your server has to be

  • An MCP server over HTTP. The store speaks Streamable HTTP: it sends JSON-RPC by POST to one address and reads the answer, as JSON or as an event stream. It does not use stdio, and it does not need a session, so a stateless server is fine.
  • On a public https address. Something like https://mcp.example.com/mcp. A private address, a plain http one, or one that redirects, is refused before anything is asked.
  • Answering in time. Listing tools and answering a call both have 25 seconds. Past that the call fails and nobody is charged.
  • Answering a bounded size. An answer over 1 MB is refused and a long text is cut at 48,000 characters, with a note that it was cut. Say what an agent needs and stop.
  • Up to 40 tools. Fewer, well described, do better than many.

Three calls are all it needs to answer: initialize, tools/list and tools/call. The example at the end is the whole of it in thirty lines.

Writing tools an agent can use

A description is all an agent has to go on. It reads it, decides whether to call the tool, and writes the input from the schema. So write each tool as if for a careful stranger.

  • A name in plain letters, starting with a letter: token_facts, send_invoice. One alphabet to a word.
  • A description that says what it does and what to pass, in one or two plain sentences. “The largest holders of a token, with labels. Pass the token’s address.” Say what comes back. Say what it is not for, if that is not obvious.
  • An input schema that is an object, with a type on every field, a description on the ones that need one, limits where they exist (maxLength, minimum, maximum, an enum of the values you take), required for what is required, and additionalProperties: false.
  • Nothing addressed to the agent. A description that tells the agent what to do, to call something else, to keep a secret, or to pass one along, is refused. So is anything that asks for a key, a password or a wallet phrase.
  • Nothing hidden. No invisible characters, no letters from another alphabet that look like ours, no HTML or markdown comments, no data: or javascript: links. A reviewer cannot see them and an agent still reads them.
  • Say who you are. Every answer from your app is signed with your developer name. A tool that reads as if it were Otis’s own, or Orbio’s, is refused.

These are the checks run the moment you press “Read my server”, and they are all shown with what failed and why. Fix what they name and press it again; nothing is submitted until you say so.

The secret, and what your server is told

When you create the app you are shown a secret once. Every call the store makes to your server carries it in the x-otis-secret header. Put it in your server and refuse any request without it, so somebody who finds your address cannot run up your costs. You can make a new one from the app’s page at any time.

The store tells your server one other thing, in x-otis-account: a tag for the account that is calling. It is the same tag every time that account calls your app and means nothing anywhere else, so you can count, rate-limit or remember per account without ever knowing who they are. Your server is never told a key, a wallet, an email or a name.

What an agent passes to your tool arrives as it was written. Treat it as text from a stranger.

Pricing a call

One price for the app, in CREDIT, charged for every call to any of its tools. Zero is free. You are paid most of each call’s price; the store keeps the rest for the review, the door, the metering and the payout.

Price it from what a call costs you. If every answer is a paid call to a data service that costs you two cents, 0.05 CREDIT covers it with room. A lookup that costs you nothing can be 0.01, or free. One CREDIT is a dollar of AI to the person paying, so an agent with a dollar of usage can make twenty calls at 0.05.

Since the price is per app, price for the average tool, not the dearest. If one tool costs you ten times the others, give it its own app.

Priced calls are paid from usage the caller has topped up. The free dollar a day does not pay for them, so a caller with no topped-up usage gets a refusal that tells them to top up, and you are not called.

Review

Submit, and two things happen. The checks above run again against your server, and what they read is frozen: the tools, their descriptions and their schemas, as a version. Then a person reads every tool. They approve it, or send it back with a note saying what to change.

What was approved is what agents get. If you change a tool on your server, the change does not reach agents until you submit a new version and it is approved; the old version keeps serving until then.

The store can take any app off the shelf. A paused app answers agents with a refusal and charges nothing, and comes back when it is resumed.

Every call is metered as it happens. Your part of the price is held for you from the moment the call is charged, and the Earnings tab shows every call and what it earned. A payout is a CREDIT transfer to the wallet on your developer profile, on Robinhood Chain, and every one is a transaction you can look up. Change the wallet from your profile at any time; payouts from then on go there.

CREDIT is Orbio’s token, and a dollar of AI when it is burnt. So what you earn can pay for your own app’s AI through Orbio, be sold on Orbio’s order book, or be held.

A server small enough to read

The whole of a working app, in Node, with one tool. It checks the secret, lists the tool, answers a call. Run it on a host with a public https address, and give that address to the store.

import { createServer } from 'node:http';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const SECRET = process.env.OTIS_APP_SECRET; // shown once when the app is created

const TOOLS = [{
  name: 'word_count',
  description: 'Counts the words in a text. Pass the text.',
  inputSchema: { type: 'object', properties: { text: { type: 'string', maxLength: 20000 } }, required: ['text'], additionalProperties: false },
}];

createServer(async (req, res) => {
  if (req.method !== 'POST') { res.writeHead(405); res.end(); return; }
  if (SECRET && req.headers['x-otis-secret'] !== SECRET) { res.writeHead(401); res.end(); return; }
  const chunks = []; for await (const c of req) chunks.push(c);
  const body = JSON.parse(Buffer.concat(chunks).toString() || 'null');
  const server = new Server({ name: 'word-count', version: '1.0.0' }, { capabilities: { tools: {} } });
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
  server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
    const n = String(params.arguments?.text ?? '').split(/\s+/).filter(Boolean).length;
    return { content: [{ type: 'text', text: `${n} words.` }] };
  });
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  res.on('close', () => { transport.close(); server.close(); });
  await server.connect(transport);
  await transport.handleRequest(req, res, body);
}).listen(4200);

Then: sign in and open Publish an app in your dashboard, paste the address, press “Read my server”, and submit.