mentos.

THE PLUGIN MAKER’S GUIDE

Your idea.
Inside Mentos.

Start with a few lines of JavaScript. Add a command, make a native page, or connect a tool to your network. No Flutter build required.

01 / START SMALL

A command in one file.

Save this as hello.js. The leading @plugin comment is its manifest: it identifies the plugin and declares the permissions it needs.

hello.js — complete installable plugin
/* @plugin {"id":"hello-plugin","name":"Hello plugin","version":"1.0.0","permissions":["commands"]} */
mentos.commands.add('hello', 'Say hello', (args) => {
  return args.trim() ? 'Hello, ' + args.trim() + '!' : 'Hello from Mentos!';
});
  1. Move the file to your phone.
  2. In Mentos, open Settings → Plugins → + → Install from file.
  3. Review the commands permission and install.
  4. In a chat, submit /hello Ada. Mentos sends Hello, Ada! to that conversation.
A command’s return value is outgoing message text. Return '' for a local action that should send nothing.

02 / GIVE IT A HOME

Two files. One identity.

For a reusable package, put a manifest and script in a folder. This second example is a persistent counter.

Repository layout
plugins/
  my-counter/
    plugin.json
    main.js
  index.json             # generated
plugins/my-counter/plugin.json
{
  "id": "my-counter",
  "name": "My counter",
  "version": "1.0.0",
  "author": "Your name",
  "description": "A counter that remembers your taps.",
  "icon": "calculate",
  "permissions": [
    "ui",
    "storage"
  ],
  "main": "main.js"
}

Use a unique ID with 2–40 lowercase letters, digits, or dashes; it must match the folder name. IDs are shared across registries, so avoid taking an existing one. Bump the version when releasing an update.

Use main.js for this site’s generator. Supported icons include calculate, note, chat, timer, code, moon, and sun. Unknown icons use a puzzle piece.

03 / BUILD SOMETHING USEFUL

A page that remembers.

Use this script with the counter manifest above. Mentos renders the nodes as native widgets and rerenders after UI callbacks. Storage survives plugin restarts on this device.

plugins/my-counter/main.js
let count = mentos.storage.get('count', 0);
mentos.ui.page({
  id: 'main', title: 'My counter', icon: 'calculate',
  render: () => [
    { type: 'stat', label: 'Taps', value: String(count) },
    { type: 'button', label: 'Add one', onTap: () => {
      count++;
      mentos.storage.set('count', count);
    } },
  ],
});

Open the installed plugin’s page and tap Add one. Restart Mentos and verify the count remains. If a timer or network callback changes page state, call mentos.ui.refresh().

Native UI building blocks

NodeFields
text / titletext; text also accepts style: body, title, heading, strong, muted, mono, error
buttonlabel, onTap, style: primary, outline, text
inputkey, label, hint, value, multiline, onChange, onSubmit
switch / sliderlabel, value, onChange; switch: subtitle; slider: min, max
listitems: [{title, subtitle, trailing, onTap}]
stat / progressstat: label, value; progress: value from 0 to 1, or omitted for a spinner
row / column / cardchildren: an array of nodes
divider / spacespace accepts size

A string is shorthand for a text node; null is skipped. Render functions return an array of nodes. Keep render functions focused on drawing UI; start network requests and timers outside them.

04 / FEEL AT HOME

Cards, tabs, and your own look.

With the ui permission, add a card to an existing screen or pin a registered page to the bottom navigation. This example extends the counter above.

main.js
mentos.ui.slot('feed.top', () => [
  { type: 'text', text: 'A small reminder for today.' },
]);
mentos.ui.tab({ title: 'Counter', icon: 'calculate', page: 'main' });

Available slots: feed.top, chats.top, profile.top, wallet.top, and network.top. Return null to hide a card.

Theme and layout

The following snippet needs both theme and layout added to the manifest’s permissions. Disabling or removing a layout plugin restores the app layout.

main.js
mentos.theme.set({
  accent: '#7C3AED',
  dark: { bg: '#0B0A1A', surface: '#13112A' },
});
mentos.layout.set({
  hideTabs: ['wallet'],
  density: 'compact',
  navLabels: false,
});

Layout also supports hiding stories, composeButton, quickActions, or marketBanner. Theme colors include bg, surface, ink, muted, accent, and chat bubble colors.

05 / CONNECT YOUR TOOL

APIs and peer messages.

HTTPS requests · net permission

Use the callback-based HTTP API. Replace the example URL with your own HTTPS endpoint; it is a placeholder, not a working service.

main.js
mentos.http.get('https://api.example.com/status', (response) => {
  if (!response.ok) {
    mentos.log(response.error);
    return;
  }
  mentos.log(response.json);
  mentos.ui.refresh();
});

The response contains ok, status, headers, body, json, ms, error. For other methods, use mentos.http.request with url, method, headers, body, timeout. Handle failure in the callback.

Peer-to-peer · p2p permission

Both contacts must have the same plugin installed. Use mentos.peers.list() to populate a contact picker, then send structured data to the selected peer.

main.js
mentos.peers.on((message) => {
  mentos.log(message.name, message.data);
});
// Call from your UI after the user chooses a contact:
function ping(contactId) {
  mentos.peers.send(contactId, { kind: 'ping' });
}

Peer messages are end-to-end encrypted and limited to 32 KB. Validate received data before acting on it. They are plugin messages, separate from ordinary chat messages.

06 / KNOW YOUR TOOLS

Permissions & API reference.

All calls below start with mentos.. Declare only the permissions your script uses. Installation asks the person to approve them; accessing an undeclared API fails.

PermissionAPI / behavior
commandscommands.add(name, description, fn)

Register a slash command; return text to send, or an empty string to send nothing.

composecompose.onSend(fn)

Transform outgoing text. Receives text and {scope}.

messages.readon('message', fn)

Receive {id, scope, from, name, text} for incoming messages.

messages.sendchats.send(scope, text)

Send a chat message.

chats.readchats.list()

List conversations with titles, peers, unread counts and last messages.

contacts.readcontacts.name(id)

Read a contact’s display name.

wallet.readwallet.balance()

Read {minor, mentos}. This does not authorize spending.

storagestorage.get / set / remove / keys

Per-plugin local storage: up to 500 keys, 64 KB each.

notifynotify(text)

Show a short in-app notice.

nethttp.request(options, callback)

HTTPS requests; GET, POST, PUT, PATCH, DELETE, HEAD. Up to 2 MB and 6 concurrent requests.

p2ppeers.list / send / on

Send up to 32 KB to the same plugin on a contact’s device.

uiui.page / slot / tab / refresh

Build native pages, screen cards and a bottom-bar tab.

themetheme.set / reset

Change colors and text scale (0.8–1.4).

layoutlayout.set / reset

Adjust tabs, screen elements, density, nav labels and feed image shape.

nonelog / me() / plugin

Log values, read the current identity, or inspect plugin metadata.

nonetimer.after / every / cancel

Seconds-based timers. Repeating timers have a 5-second minimum; 20 timers maximum.

A JavaScript subset, inside a sandbox

Use variables, functions, closures, arrays, objects, loops, template strings, optional chaining, and common String, Array, Math, JSON, Object, and Date.now helpers. Browser and Node APIs are unavailable: no imports, DOM, eval, direct filesystem access, or global fetch. Use the Mentos APIs instead.

Execution has a step budget. Keep callbacks short; use timers for delayed work. Timers run while the plugin runtime is active, so do not promise background or closed-app reminders.

07 / TRY IT FOR REAL

Install. Inspect. Iterate.

Open Plugins → your plugin → Log to inspect errors and mentos.log output. Test the behavior in the actual app; passing a browser JavaScript check does not prove compatibility with the interpreter.

  • Exercise commands and every button, including empty or invalid input.
  • Restart the app to check persistence; disable the plugin to check cleanup.
  • Check both themes and long labels on a narrow screen.
  • For HTTP and peer tools, test unreachable services and missing peers.

Develop against a running app

Enable Settings → Developer. The dashboard at http://127.0.0.1:8787 includes plugin logs, reload, and enable controls. Push a single-file plugin using the app’s access code:

Terminal — replace the access-code placeholder
curl -X POST http://127.0.0.1:8787/v1/plugins -H "Authorization: Bearer <access code>" --data-binary @hello.js

Run this where the app’s local developer endpoint is reachable. On a separate phone, localhost refers to the phone; use your existing device forwarding setup. Keep the access code out of published files.

08 / SHARE YOUR WORK

From your device to Discover.

Add the plugin folder to either repository, then regenerate that repository’s index.

In the mentossite repository
npm run plugins:build
In the mentos app repository
dart run plugins/build.dart

Publish the manifest, script, and generated plugins/index.json together. This site reads the registries from both dagmawibabi/mentos and dagmawibabi/mentossite. A plugin is not listed remotely until its registry entry is published.

Keep the bytes consistent. Use LF line endings and regenerate the index after any script change. The SHA-256 pin must match the exact published script. This site’s generator normalizes line endings; do not edit generated checksums by hand.

For updates, increase the manifest version, rebuild the registry, and publish both. Discover offers the update and highlights new permissions. Test an install from the published registry as well as your local file.

Host your own registry

Serve an index over HTTPS. Each entry includes the manifest metadata, script SHA-256, and a manifest path relative to the index URL. Users add it in Plugins → + → Registries.

Explore working plugins ↗