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.
/* @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!';
});- Move the file to your phone.
- In Mentos, open Settings → Plugins → + → Install from file.
- Review the commands permission and install.
- In a chat, submit
/hello Ada. Mentos sends Hello, Ada! to that conversation.
'' 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.
plugins/
my-counter/
plugin.json
main.js
index.json # generated{
"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.
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
| Node | Fields |
|---|---|
text / title | text; text also accepts style: body, title, heading, strong, muted, mono, error |
button | label, onTap, style: primary, outline, text |
input | key, label, hint, value, multiline, onChange, onSubmit |
switch / slider | label, value, onChange; switch: subtitle; slider: min, max |
list | items: [{title, subtitle, trailing, onTap}] |
stat / progress | stat: label, value; progress: value from 0 to 1, or omitted for a spinner |
row / column / card | children: an array of nodes |
divider / space | space 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.
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.
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.
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.
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.
| Permission | API / behavior |
|---|---|
commands | commands.add(name, description, fn)Register a slash command; return text to send, or an empty string to send nothing. |
compose | compose.onSend(fn)Transform outgoing text. Receives text and {scope}. |
messages.read | on('message', fn)Receive {id, scope, from, name, text} for incoming messages. |
messages.send | chats.send(scope, text)Send a chat message. |
chats.read | chats.list()List conversations with titles, peers, unread counts and last messages. |
contacts.read | contacts.name(id)Read a contact’s display name. |
wallet.read | wallet.balance()Read {minor, mentos}. This does not authorize spending. |
storage | storage.get / set / remove / keysPer-plugin local storage: up to 500 keys, 64 KB each. |
notify | notify(text)Show a short in-app notice. |
net | http.request(options, callback)HTTPS requests; GET, POST, PUT, PATCH, DELETE, HEAD. Up to 2 MB and 6 concurrent requests. |
p2p | peers.list / send / onSend up to 32 KB to the same plugin on a contact’s device. |
ui | ui.page / slot / tab / refreshBuild native pages, screen cards and a bottom-bar tab. |
theme | theme.set / resetChange colors and text scale (0.8–1.4). |
layout | layout.set / resetAdjust tabs, screen elements, density, nav labels and feed image shape. |
none | log / me() / pluginLog values, read the current identity, or inspect plugin metadata. |
none | timer.after / every / cancelSeconds-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:
curl -X POST http://127.0.0.1:8787/v1/plugins -H "Authorization: Bearer <access code>" --data-binary @hello.jsRun 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.
npm run plugins:builddart run plugins/build.dartPublish 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.
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.
mentos