For teams whose registration flow publishes plugin catalog entries: engine9 (Engine9Workers.registerPluginCatalog), Frakture (BotFactory.register), and any other publisher.
market.engine9.ai keeps one catalog entry per plugin version and serves it publicly. Secrets tools, task callers and settings screens read the entry instead of your code. Most importantly, market publishes each plugin's credentials definition, the URL that secrets.engine9.ai uses to know which fields a connection needs and which are secret.
The entry format is core's standard, not market's: docs/plugin-catalog.md in @engine9/core 1.20.1 or later.
| URL | |
|---|---|
https://market.engine9.ai/plugins/<path>.json | Your newest entry |
https://market.engine9.ai/plugins/<path>@<version>.json | The entry for one version. Never changes |
https://market.engine9.ai/definitions/<path>.json | The credentials definition. Use it as definition_source in secrets |
https://market.engine9.ai/definitions/<path>@<version>.json | The definition as of one version |
https://market.engine9.ai/plugins/<path> | A page for people |
<path> is the plugin path, the same path install uses, written as is:
https://market.engine9.ai/definitions/@frakture-com/channelbots/ActBlueBot.json
https://market.engine9.ai/definitions/@engine9/plugins/e9email.json
The definition is made from the entry's credentials with core's pluginCredentialsDefinition. You never write it yourself. A plugin with no credentials has no definition (404).
engine9 sends you an invite link (https://market.engine9.ai/invite/…). Open it in a browser and press Create my key. Your key (e9mkt_…) is shown once, on that page; engine9 never sees it and keeps no copy. The link works once and expires after 7 days.
Each key may publish only its publisher's packages, for example:
| Publisher | Key may publish |
|---|---|
| engine9 | @engine9/* |
| Frakture | @frakture-com/* |
Keep the key where your deploys keep secrets (secrets.engine9.ai or your CI's secret store) and give it to the registration flow as E9MARKET_KEY. Never commit it or log it. If it is lost or leaks, ask engine9 for a new invite; your old key keeps working until engine9 revokes it, so you can switch without a gap.
Check it:
curl -s -H "Authorization: Bearer $E9MARKET_KEY" https://market.engine9.ai/v1/whoami
# { "name": "frakture", "packages": ["@frakture-com/*"] }
Your registration flow already builds entries in the standard shape (core's buildPluginCatalogEntry, or your builder that writes the same thing). Before publishing, each entry needs:
path, starting with package.name.package.version: the npm version of the package the plugin ships in. This is the only version a plugin has.name and publisher.name.credentials.fields for anything a person enters to connect, each with secret: true or false. Keys, tokens and passwords are secret; ids and regions a person may read back are not. No credential goes in settings.Check before you publish, locally or against market (no key needed for this):
npx e9core catalog validate # in a package
curl -s -X POST -H 'Content-Type: application/json' --data @entry.json https://market.engine9.ai/v1/validate
Send all of a release's entries in one request: an array of entries, or a bundle { "<path>": entry }.
// At the end of registration, after the entries are built.
const res = await fetch('https://market.engine9.ai/v1/plugins', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.E9MARKET_KEY}` },
body: JSON.stringify(entries) // [entry, …], up to 500 per request
});
const { published, unchanged, failed, results } = await res.json();
for (const r of results) if (r.http_status >= 400) console.error(`market: ${r.path}: ${r.error}`, r.errors || r.message);
Or one entry at a time: PUT https://market.engine9.ai/v1/plugins/<path> with the entry as the body.
From a shell, with entry files or bundles (e9core catalog build output, engine9-plugins.catalog.json, frakture-bots.catalog.json):
E9MARKET_KEY=e9mkt_… node scripts/publish.mjs catalog/ # in the engine9-ai/market repository
Each entry in results has http_status, and status, url, version_url, definition_url and warnings when it worked. A batch answers 200 when every entry worked and 207 when any failed.
http_status | Meaning | What to do |
|---|---|---|
201 published | New version stored. newest: true when it is now the newest | Nothing |
200 unchanged | This version is already published with the same content | Nothing. Registering again is safe |
409 version_exists | This version is already published with different content | Bump the package version. A published version never changes |
422 invalid_entry | The entry does not meet the standard. errors lists why | Fix the builder or the plugin's metadata |
403 forbidden | The key may not publish this package | Check package.name, or ask for the key's packages to change |
401 unauthorized | Missing, wrong or revoked key | Check E9MARKET_KEY |
/v1/validate before deploy.definition_source https://market.engine9.ai/definitions/<path>.json pick up new fields the next time a person edits them. A field that was ever secret stays secret, even if a later entry says otherwise. Fields your plugin writes for itself (an OAuth refresh token) need not be declared; secrets treats them as secret.const { SecretsClient } = require('@engine9/secrets-client');
const secrets = new SecretsClient();
const definition_source = 'https://market.engine9.ai/definitions/@frakture-com/channelbots/ActBlueBot.json';
await secrets.request('acme.actblue', { definition_source, message: 'Connect ActBlue' }); // a link for a person
await secrets.set('acme.actblue', value, { definition_source }); // or store directly
@engine9/plugins/<name>, @engine9/schemas/<name>.Engine9Workers.registerPluginCatalog builds the entries; add the publish call after it, with the key from the worker's secrets. Plugins that run on engine9's own servers list their methods the same way as installed ones.@frakture-com/channelbots/<BotName>, with bot_path set to the Frakture bot id (channelbots.ActBlueBot).BotFactory.register converts each bot's auth_fields to credentials and its mcp_methods (or catalog_methods) to methods. Check that every converted field has secret set the way you mean it: an untyped legacy auth field becomes a secret password.register produces.Ask in the engine9 team channel. The entry standard is in @engine9/core; changes to it go there, not to market.