How Ahadi works
A language model talks. A rule decides. Two keys sign. This page shows each part, where it runs, and the evidence that it behaves the way we say it does.
The system at a glance
Omega agent
SingularityNET Omega v0.1.20 image with the main branch source at commit 63a7826, running in Docker. It talks over Omega's websocket channel to the bridge. The Ahadi plugin adds ten skills and the MeTTa funding rules.
Bridge
A FastAPI service. It serves the public API and live stream, gives Omega its skill tools, re-checks every decision with a Python mirror of the rule, signs with the agent's key, and relays Telegram.
Stellar testnet
Each event's fund is a 2 of 2 vault. Payouts are payments, prizes for wallets that cannot receive yet are claimable balances, and the rule's hash sits on the fund as a manageData entry.
Who does what
| Part | Does | Never |
|---|---|---|
| The language model, through Omega's provider | Reads messages, chooses which skill to call next, writes replies and explanations. | Picks an amount, decides eligibility, or signs. |
| MeTTa rules, in Omega | Derives every amount, runs the nine checks, chooses the route, decides the next step after a failure. | Talks to people. |
| Omega memory | Remembers requests, payouts and failures across sessions and events. | Overrides the rule or the ledger. |
| Omega skills | The only way the agent acts: ten ahadi skills that call the bridge. | Reach a shell, write files or search the web. Those skills are removed. |
| Bridge | Supplies facts, re-runs the rule in Python, signs with the agent's key only when both agree, submits, keeps the ledger. | Proposes anything MeTTa refused, or that the two disagree on. |
| Stellar testnet | Enforces two signatures, records receipts, holds the rule's hash. | Knows the rule itself. It only sees the hash. |
| Organiser | Attests who qualified, approves or rejects each payout, signs in the browser. | Pays alone. |
The money gate
ahadi-decide in MeTTa. Nine checks must all pass. Then the bridge runs the same rule in Python and compares.1. MeTTa decides
The facts arrive from the bridge as one expression. The decision goes back with every check named.
; facts = (ahadi-facts published rule-match attested place-ok expected requested
; asset-ok budget-left seen place-paid active trusts other-chain)
(= (ahadi-decide (ahadi-facts $pub $match $att $pok $expected $requested $aok
$left $seen $paid $active $trusts $other))
(let $checks ((ahadi-check rule-published $pub)
(ahadi-check rule-version $match)
(ahadi-check eligibility-attested $att)
(ahadi-check place-exists $pok)
(ahadi-check-amount $requested $expected)
(ahadi-check asset $aok)
(ahadi-check-budget $requested $left)
(ahadi-check not-duplicate $seen)
(ahadi-check place-unpaid $paid))
(if (ahadi-all-passed $checks)
(ahadi-decision allowed (ahadi-route $active $trusts $other) $checks)
(ahadi-decision refused none $checks))))
; A wallet that cannot receive yet gets the prize held (claimable balance), never lost.
(= (ahadi-route $active $trusts $other-chain)
(if (== (* $active $trusts) 0)
claimable
(if (== $other-chain 1) cross-chain-sim direct)))2. The bridge must agree
Same facts, Python mirror of the same rule. A proposal is created only when MeTTa allows it and the two decisions match, route included.
# what Omega decided, and the Python mirror on the same facts
metta = _parse_metta(body.get("metta_decision", ""))
py = decide(engine.rule(eid), req, facts)
py_allowed = bool(py and py.allowed)
agree = (metta["allowed"] == py_allowed) and (
not py_allowed or metta["route"] == py.route)
hub.publish("decision", metta=metta, agree=agree, ...)
if metta["allowed"] and agree:
engine.propose_payout(eid, req, ...) # unsigned proposal
else:
# refused, or the two disagree: record why
reasons.append("MeTTa and the bridge disagreed, "
"so nothing was signed")Agreement is not the last word. When the organiser opens a proposal to sign, prepare runs the rule again against the facts at that moment and refuses if anything changed.
The nine checks
rule-publishedThe rule's hash is recorded on chain.
rule-versionThe request names that exact rule.
eligibility-attestedThe organiser has said how many teams qualified.
place-existsThe place is in the rule, and the wallet is the one registered for it.
place-amountThe amount is exactly what the rule gives that place.
assetIt pays in the rule's asset.
budgetIt fits in what is left of the award pool.
not-duplicateThis exact request has not been seen before.
place-unpaidNobody has been paid for this place yet.
If all pass, the route is direct, or claimable when the wallet cannot receive yet, or a simulated cross-chain leg when the winner asked for another chain.
- The MeTTa derivation of the October award pool, reserve and six prizes matched the bridge.
- All six MeTTa payout decisions agreed with the bridge.
- The full scenario ledger balances against the fund's balance on chain, to the cent.
Two keys, one vault
Each event's fund is a Stellar account set up by scripts/setup_testnet.py in one transaction signed by its own key: add Ahadi and the organiser as signers, then switch the account's own key off. The script reads the configuration back from Horizon before the fund is used.
- weight 1Ahadi's keyLives in the bridge. Signs only what the rule allows.
- weight 1Organiser's keyLives in the organiser's browser.
- weight 0The fund's own keySwitched off at setup.
With every threshold at 2, nothing can happen on the fund, not even changing its signers, without both keys. The October ledger shows it live: a single key line is Ahadi trying to pay with its key alone, refused with tx_bad_auth, no fee charged.
Tested on the live testnet
7 of 7 passedEach run builds a throwaway vault with Friendbot, configured exactly like the real funds. Source: bridge/tests/test_chain_vault.py, run with uv run pytest -m chain.
- Configured
test_vault_is_two_of_twoRead back from Horizon: thresholds 2, 2, 2; the fund's own key weight 0; agent 1; organiser 1.
- tx_bad_auth
test_agent_alone_cannot_payA payment signed only by Ahadi is refused, and the vault's sequence number does not move.
- tx_bad_auth
test_organiser_alone_cannot_payA payment signed only by the organiser is refused.
- tx_bad_auth
test_disabled_master_cannot_payThe fund's own key, switched off, cannot pay either.
- Settled
test_both_signatures_payWith both signatures the payment settles, both signers are on the envelope, the memo is a hash, and exactly USD 2.50 arrives.
- Paid once
test_resubmitting_same_envelope_does_not_double_paySending the same signed envelope twice returns the same hash and moves the money once.
- op_no_destination, then held
test_unactivated_destination_fails_then_claimable_worksPaying a wallet that was never activated fails; a claimable balance to the same wallet succeeds.
A request's lifecycle
- RequestedA skill call or a message
- Checked by the ruleMeTTa and the bridge must agree
- Refused by rule. A rejected line with reasons. Nothing is signed.
- Duplicate. Points to the original receipt. Nothing new is sent.
- ProposedWaits for the organiser
- Rejected by the organiser. A rejected line with the organiser's reason.
- Agent signedprepare re-checks the rule, then signs
- Organiser signedIn the organiser's browser
- SubmittedSigned XDR stored before it is sent
- Failed. The network refused it. Nothing moved.
- Rerouted, then held. A claimable balance for 30 days, signed by both keys again.
- SettledA paid line with its receipt
Paying exactly once
- 1
A fingerprint for every request
The request (event, rule hash, place, team, recipient, asset, amount in cents) is written as canonical JSON, keys sorted, no spaces, and hashed with SHA-256. Same request, same id.
- 2
The fingerprint travels with the money
It is the transaction's hash memo, so the chain itself can be searched for it, and anyone can match a receipt to its ledger line.
- 3
The rule refuses repeats
not-duplicatefails when the id already has a ledger line or a live proposal. A repeat becomes a duplicate line that points to the original receipt. - 4
The signed transaction is stored before it is sent
If the answer is lost, the bridge looks the hash up on Horizon instead of signing something new. Resubmitting the same envelope is safe: Horizon returns the original result, and the money moves once. A testnet test proves it.
{"amount_cents":17000,"asset":"USDB","event_id":"october-2026","place":1,"recipient":"GATT7DEYRM2GYSCKBUY4U22GKMS2I7EKWK3F4R356SS2CTXDNWQF4PHR","rule_hash":"4ea80d7c415bf35a0a9e4baa8647e15351bb85e56dece2cbf9273aec20bd9333","team":"Team Mvua"}sha256 of the request, computed for this page
d720ddf847393d19620c790ee822c5be93afb4abf37ae032549f974723516494
request id in the ledger, and memo on transaction ab4e8a83…5a76
d720ddf847393d19620c790ee822c5be93afb4abf37ae032549f974723516494When the network says no
| Result code | In plain words | What Ahadi does |
|---|---|---|
op_no_destination | This wallet has never been activated on the network. | FailedHeld for winner Records the failure, then proposes holding the prize as a claimable balance for 30 days. It returns to the fund if unclaimed. |
op_no_trust | This wallet cannot hold the prize asset yet. | FailedHeld for winner Same as above: the prize is held until the winner can receive it. |
tx_bad_auth | Only one of the two required signatures was present. | Single key refused Expected in the single key demonstration. Nothing moves and no fee is charged. The approve step also refuses to submit without both signatures. |
tx_too_late | The approval came after the time limit on this transaction. | Failed MeTTa's next step is to propose again. Transactions carry a 15 minute time bound and are built only when the organiser prepares to sign. |
tx_bad_seq | The fund's transaction sequence moved on; this transaction is out of date. | Failed Propose again. Building at prepare time keeps waiting proposals from colliding on the sequence number. |
The next step after a failure is a MeTTa rule too. ahadi-reroute reads the proposal's result code and asks ahadi-next-step; only an answer of claimable leads to a held prize. The bridge enforces the same table before it builds anything.
In the October run, Arjun's wallet in Chennai had never been activated. His USD 108.80 failed with op_no_destination, then settled as a held prize. Both lines are on the public ledger.
(= (ahadi-next-step op_no_destination) claimable)
(= (ahadi-next-step op_no_trust) claimable)
(= (ahadi-next-step op_not_authorized) claimable)
(= (ahadi-next-step tx_too_late) re-propose)
(= (ahadi-next-step tx_bad_seq) re-propose)What Omega remembers
Long-term memory
Omega's remember and query skills, stored in ChromaDB on the memory volume
After each money action the agent stores a one line fact, such as PAID october-2026 place 2 Team Alfama USD 136.00 receipt …
Episodic history
history.metta on the same volume
A timestamped trail of every message the agent received and every skill it called. The console's agent view reads it.
Money state
The bridge's SQLite ledger and Stellar testnet
What was paid, held, refused and carried forward. Duplicate checks run here, deterministically.
A fresh session still remembers. Restarting the Omega container clears the conversation, but the memory volume persists. The bridge does not replay old conversations to a fresh Omega process, so anything the agent recalls about an earlier event comes from its own long-term memory, confirmed against the ledger with ahadi-status.
You can read exactly what Ahadi remembers at any time: What Ahadi knows, or GET /api/agent/memory.