# The Orb Bot Registry

> Monad testnet only (chain 10143). Reviewed October 10, 2026. The test token (ORB) has no market value and is not an investment; nothing here is a sale, a yield or a promise of one. The registry is **optional**: an Orb app runs exactly the same without a record on it.

The registry is a small public contract where a developer can put an Orb app on the record, and where a community can say it accepted one. It answers three questions that a database row cannot answer to a stranger:

- **Who wrote this app?** Whoever signed its registration, with their own wallet. The app's id is derived from that wallet, so nobody can register an id on someone else's behalf.
- **What does it commit to?** A hash of its manifest (name, scopes, global commands, homepage). Anyone can rebuild the manifest from the public listing, hash it and compare.
- **What did this community accept, and what did it pay?** The scopes the community saw when it installed the app, who signed, and whether the one-time price was paid, all kept as they were even if the app later asks for more.

It is deliberately not a store. Nothing in the contract runs an app, pauses it, upgrades it or takes it back, and Orb holds no key that can change a record.

## The deployment

| | |
| --- | --- |
| Contract | `OrbBotRegistry`, source in `contracts/OrbBotRegistry.sol` |
| Address and bindings | `public/bots/registry.deployment.monad-testnet.json` (also readable by anyone from the chain) |
| Payment token | the Orb test token (ORB), fixed at deployment |
| Community registry | the Arena community registry, fixed at deployment |
| Fee recipient | the Orb team wallet, fixed at deployment |
| Fee | 5% (`FEE_BPS = 500`) of a price, taken from the price, never on top of it |
| Largest price | 1,000,000 ORB |

The address, the token, the community registry and the fee recipient are `immutable`: they are written into the code when the contract is deployed. Orb's own reader (`lib/bot-registry.ts`) checks the deployed code against the build in `public/bots/OrbBotRegistry.json` with those bytes blanked out, then checks the three values through the contract's own getters, before it signs or believes anything.

Rough cost on testnet, from the contract test: registering an app about 164,000 gas, a free install about 106,000, a paid install about 194,000 (Monad charges the gas limit, so the wallet asks for a limit close to the estimate). At the current testnet price that is a couple of hundredths of a test MON.

## Identifiers

Everything is computed the same way on the client, on the server and by anyone reading the chain:

```
localId     = keccak256(utf8("orb:app:" + <the app's id in Orb>))
appId       = keccak256(abi.encode(developerWallet, localId))      // the contract's own appIdOf
communityId = keccak256(utf8("orb:community:" + <the community's id in Orb>))   // the same ids Arena uses
```

Because `appId` includes the developer's wallet, two developers who happen to use the same `localId` get different apps, and neither can touch the other's.

## The manifest

The record commits to the keccak256 of this JSON, written with no whitespace, keys in exactly this order, `scopes` and `commands` sorted alphabetically without repeats:

```json
{"format":"orb-app-manifest-v1","name":"Poller","scopes":["messages:read","messages:write"],"commands":["poll","stats"],"homepage":"https://example.org/poller"}
```

`commands` lists the app's **global** commands (those a community did not narrow to itself). The public directory (`GET /api/orb/directory`) and the invite page for each listed app show this exact JSON and its hash, so the check is: hash the JSON with keccak256, then compare with `apps(appId).manifest` on the contract.

Scopes are stored as a bitmask in this order: `messages:read` = 1, `messages:write` = 2, `voice` = 4, `music` = 8, `passport` = 16 (`SCOPE_MASK` = 31).

## What each function does

| Function | Who | What it does |
| --- | --- | --- |
| `register(localId, manifest, scopes, price, name, uri)` | the developer's wallet | Creates the record. The name is 1 to 32 bytes, the `uri` (the app's invite page) at most 200 bytes. |
| `update(appId, manifest, scopes, price, uri)` | the developer who registered it | Changes the manifest, scopes, price and link. Communities that already installed keep what they accepted. |
| `retire(appId)` | the developer who registered it | Stops new installs for good. Communities that already installed keep their record. |
| `install(appId, community)` | the community's owner | Records the install with the scopes the app asks for right now. If the app has a price and this community never paid, takes it once: 95% to the developer, the fee to Orb. |
| `uninstall(appId, community)` | whoever installed it, or the community's registered owner | Takes the record down. Coming back never charges again. |
| `claimFees()` | the Orb fee recipient only | Moves the fees the contract holds to the recipient. |

Rounding: the fee is floored, so a price of 19 wei has no fee and 39 wei has 1 wei. The developer receives the rest to the wei. A later change of price never reaches a community that already paid.

**Who is the "community's owner"?** If the community is registered in the Arena community registry, only its registered owner can install or uninstall it here. If it is not, an install is a claim by that wallet, and a reader decides how far to trust it. Orb treats it as a claim: nothing on the chain makes an Orb community install an app.

## What Orb's server does with it

The chain is the authority; the server keeps a remembered reading (`chain_app`, `chain_price`, `chain_retired` on the app) so a listing can show a price without a network call.

- An app that was **never registered** costs nothing to install and the server never looks anything up.
- An app with a **price**: before installing it into a community, the server reads the registry fresh and requires that the community's payment (`purchased`) is on chain. The developer's own communities never ask the developer to pay for their own app. The price in the panel is whatever the chain says now, not a cached number.
- A **retired** app cannot be installed any more and leaves the public directory.
- If the chain **cannot be read**: a free app installs anyway, and a priced app waits ("a price that cannot be checked is not waived").
- The server takes no key and signs nothing. Registering, updating, retiring, paying and installing are transactions the person signs with their own Orb Wallet from the Apps panel, each reviewed, simulated and capped on fee before anything is sent, and each remembered in the browser before it is sent so a lost answer never becomes a second payment.

## Checking a record without Orb

With any Ethereum library and the public RPC, for the app whose id and developer wallet you know:

```js
import { ethers } from 'ethers';
import registry from './OrbBotRegistry.json';   // public/bots/OrbBotRegistry.json
const rpc = new ethers.JsonRpcProvider('https://testnet-rpc.monad.xyz');
const contract = new ethers.Contract(REGISTRY_ADDRESS, registry.abi, rpc);

const appId = await contract.appIdOf(developerWallet, ethers.keccak256(ethers.toUtf8Bytes('orb:app:' + orbAppId)));
const record = await contract.apps(appId);                 // developer, scopes, price, created, retired, manifest, name, uri
const listed = ethers.keccak256(ethers.toUtf8Bytes(manifestJsonFromTheListing));
console.log(listed === record.manifest);                   // true when the listing is what the developer committed to
const accepted = await contract.installs(appId, communityId);   // scopes, since, by, active
```

## What this is not

- Not a requirement: an app works without it, and Orb never moves a community off an app because of it.
- Not a review: a record says who signed and what they committed to, not that the app is safe or good. Install what you trust, and read the scopes.
- Not custody: the contract holds only the fees from paid installs until the Orb fee recipient claims them. It cannot touch an app's earnings, which go straight to the developer's wallet when a community pays.
- Not a market: the price is paid in a testnet token with no value, and there is no resale, ranking or incentive attached to it.
- Not a way to control an app: there is no admin, pause, upgrade or take-back function.

## Verifying it

```bash
npm run bots:registry:test          # contract rules, then the reader and the server's side of it, on a local EVM
npm run bots:registry:deploy:check  # dry run of the testnet deployment
```

The tests cover authorship bound to the registering wallet, scopes kept as accepted, protection of registered communities, the 95/5 split to the wei with floored fees and no double charge, retirement and fee claims, a deployment that is not the configured one being refused, the paid-install gate reading the chain fresh, free apps never blocked by a silent chain, and the manifest in the public directory hashing to the record.
