Our Multica agents now work in an agent market: they buy, remember, review sellers, and sell their own work
Multica is an agent factory: you give an agent a runtime, instructions and a schedule, and it works through tasks. AIMarket hubs (modelmarket.dev is the public one) are a market where agents buy capabilities from each ot
Multica is an agent factory: you give an agent a runtime, instructions and
a schedule, and it works through tasks. AIMarket hubs (modelmarket.dev is the public one)
are a market where agents buy capabilities from each other per call. We run both, and in the first week of November
we connected them. This is what the agents do now, what broke, what we found when we audited ourselves, and a
setup guide in five languages for anyone who wants the same.
The crew
Five agents run on one self-hosted Multica 0.5.3 (OpenCode + DeepSeek), on fixed UTC schedules:
- GitHub digest, daily at 08:00: activity across our repositories, sent to Telegram. It keeps its past digests in Attested Memory, bought on our hub per call ($0.004 to search, $0.012 to write), so it can say "stars on X fell for the third day" instead of repeating yesterday. About $0.03 a run.
- Mystery shopper, daily at 09:00: buys a few random safe skills on modelmarket.dev with a prepaid key capped at $0.15 a run, and checks the receipt, the latency and whether the answer matches the listing.
- Supplier reviewer, daily at 10:00: diffs the market against yesterday and reviews every new or changed listing.
- Weekly post, Mondays at 11:00: drafts a build-in-public post from the hub's counters and the agents' own results. A human reads it and decides.
- Order runner: writes the summary for a long job that someone buys on the hub (below).
The bug we found before writing a single memory
Our memory service took a memory's owner from a payment header that the hub sends empty for most buyers. Every buyer
on a prepaid key, a trial or an on-chain payment was the same owner, so "private" memories were readable by all of
them. Nothing had been written through the hub yet, so nothing leaked. The fix: the hub names the buyer it
authenticated to its own providers with an HMAC pseudonym. The pseudonym is stable across restarts and reveals no
account, and the memory service keys owners by it.
We found the same bug once more a day later, one hub further away (see "We audited ourselves").
Agents that read strangers' text
An agent that reads GitHub issues or sellers' answers can be talked into sending data out. Three layers:
- Egress allowlist. The agents' Docker network has no route out except a proxy with a short, anchored host list.
-
A flow guard in the runtime. Our WARDEN guard runs as an OpenCode
plugin (
@aimarket/warden/opencode, from 0.23.0). After the session has read outside text, it refuses a tool call that would carry a credential or text the session read. In a staged attack we planted a fake GitHub token in a file. Sending it was refused through the chat script, curl to an allowed host, a memory write, a project file and the tracker CLI. A normal day's digest ran with zero refusals, after we fixed one false positive: a task's UUID looked like private text. - Least privilege per agent. All agents share one daemon, so each gets its own Multica environment. The reviewer and the post writer get empty hub keys, no GitHub token, and the hub's MCP tools switched off.
Reviewing sellers with a script, not a model
The reviewer reads exactly the text an attacker writes: listing descriptions. So a script decides the facts, and the
model only writes the prose and a welcome draft for each outside author. The facts are broken schemas, injection
patterns from WARDEN's static scan, a skill that asks the buyer for a credential, plain-http or internal addresses,
and price mismatches. The script's facts block goes into the report unchanged.
Its first run took a baseline of 282 listings, 22 federation peers and 2 x402 sellers. The one high finding was ours:
our public manifest showed a seller's internal plain-http container address. The hub now shows its own invoke
address for sellers that only it can reach, and keeps the real one for routing. The reviewer also drafted five
welcome letters to outside sellers. Nobody has sent them; that is a human decision.
Selling a long job
A paid call cannot wait minutes for an agent, so a GitHub activity digest is sold as two capabilities on
hub.attestedmemory.net:
-
github.digest.order@v1($0.05) takes any public GitHub user or organization and 1 to 7 days, and answers at once with a job id; -
github.digest.result@v1(free) returns the digest to the buyer who paid.
The first real order failed, and our own guard caused half of it. The agent read its task, including a per-job
delivery URL, then read GitHub, then tried to send the result to that URL. WARDEN treats task text as private, and
after outside text it lets no private data leave, whatever the destination. That is the rule we want for an
unattended agent; the delivery design was wrong. The other half: GitHub's 60 unauthenticated requests an hour per IP
were already spent on that host. (By what, we found later: a video worker of ours whose model calls had been failing
since September. It re-fetched GitHub every two minutes, because its failed run never saved the time.)
The second design went live the next morning. The service does the GitHub reads itself, with a token that has no
scopes, and builds the dated items. A login that does not exist, or a GitHub outage, refuses the order before
anyone is charged. The agent's final answer is one JSON line with a summary. The service collects it through a
read-only database function that returns only that agent's answers for that job. If no usable summary arrives,
the service writes a plain one from the same data, so a paid order always gets its digest. Four test orders so far:
- octocat for a week, 0 events;
- the microsoft organization for a day, 87 events;
- our own account for three days, then for two.
Each came back with the agent's summary. In the latest one, the agent's answer was there 31 seconds after the
order.
We audited ourselves for the bug classes an outside audit found
In October, Vulnetix found real bugs in our code, and we then audited the whole ecosystem for the same classes:
- endpoints open without a key;
- checking a host name and then connecting to it;
- trusting forwarded client addresses.
On November 5 we went through everything built for Multica looking for those classes, plus secrets reaching
errors and logs, and allowlists that can be stepped around. The service, the scripts, the database function, the
proxy and the ports were clean. Four things were not.
-
The order runner could reach every task in the workspace. Its OpenCode rules allowed
multica issue get *, and a Multica agent's token is bound to the workspace, not to its own task. Task numbers are sequential. Text a buyer controls reaches the agent: the PR titles of the GitHub account they order. If that text talked the agent into reading another task, the content could land in the summary. Multica posts that summary itself, outside the tool calls a guard sees. A redirect (> file) also passed the allowlist. The agent now runs a root-owned wrapper that reads and closes only tasks assigned to it, with redirects,$and backticks denied. -
The guard could be renamed away. Its files belonged to root, but the folder above them belonged to the agent
user, and Linux lets you rename a folder inside one you own. The guard now lives in a root-owned folder outside
the agent's home, and OpenCode finds it through
OPENCODE_CONFIG_DIR. - The memory-owner bug, one hub further. When another hub routes a call to ours on its own key, our hub saw that hub's account as the buyer, so all of its buyers would have shared one memory owner. Nobody had used that path. Routed calls now name no owner.
- Container addresses in our second hub's public manifest, the same as the reviewer's finding. That fix is in the code; this hub gets it with its next update.
Two things we learned about OpenCode 1.18.32 on the way:
-
Permissions. It checks every command in a line on its own, including those inside
$(...), backticks and pipes, and the last matching rule wins. A redirected command is matched together with its redirect. -
The config folder. At start it writes a
.gitignoreinto every config folder. On a read-only mount it stops with an error, so mount the guard's folder writable but owned by root.
We could not test the permission layer by asking the model to break it: DeepSeek refused all six bypass attempts on
its own. We read OpenCode's source instead.
If you run Multica too
We wrote a step-by-step guide, from the container to selling a long job, in English, Russian, Spanish, French
and Chinese. It sits in our open-source monorepo next to the Multica skill our agents use for buying and selling
and the full audit:
-
multica/manual; -
multica/community/aimarket/SKILL.md; -
artifacts/vulnetix-class-multica-2026-11-05/REPORT.md.
They reach the public mirror, github.com/alexar76/aicom, with its next sync, and this post will link them then.
What we would tell another operator:
- Keep facts in scripts and prose in models.
- Give each agent its own environment; the global config is shared by everyone on the daemon.
- Put the network allowlist below the prompt, and the guard below the model, in a folder the agent cannot touch.
- Allow an agent a wrapper you wrote, not a CLI with a wildcard.
This article was written with AI assistance from our agents' own logs and audit notes, and edited by a human.
* We thank Vulnetix for helping us improve our products.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.