Skip to content
Stellar testnet. No real money moves. Cross-chain quotes are simulated. All people and teams are sample data.

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

Four parts, each with one job. The web app shows things. The bridge keeps the ledger and the agent's key. Omega, in its own container, reasons and talks. Stellar testnet holds the money and enforces the signatures.
Ahadi architectureBrowsers reach the Next.js web app, which proxies /api to the bridge. The organiser's browser holds the organiser's key and signs. The bridge talks to the Omega container over a websocket channel and serves its skill tools, keeps the ledger and the agent's key, and talks to Stellar testnet and to NEAR Intents for dry quotes only.BrowserLedger, receipts, docsAnyone, no accountOrganiser's browserHolds the organiser's key,imported from a local fileSigns the XDR locallyTelegramParticipants message AhadiNext.js web appPublic pages and theorganiser consoleProxies /api to the bridgeRuns stellar-sdk in thebrowser for signingBridgeFastAPI, holds no LLMREST/api/*Live stream (SSE)/api/streamWebsocket channel/omegaSkill tools/tools/*Python mirror of the rulerules.pyLedgerSQLiteAgent's key, one of twoAlso relays Telegram to and from OmegaOmega containerDocker, unprivileged user,no-new-privileges, LandlockAgent loopLLM via Omega's providerMeTTa rulesahadi-decideSkills10 ahadi-*Long-term memoryChromaDBEpisodic historyhistory.mettaNo keys. Shell, file and websearch skills removed.Stellar testnet2 of 2 vault funds, payments,claimable balances, manageDataholding the rule hashNEAR Intents 1ClickDry quote only.No funds move./apiBot API, relayed by the bridgemessagesskillsHorizondry quote
Scroll sideways to see the whole diagram. Arrows show who calls whom. The organiser's key never leaves the organiser's browser; the agent's key never leaves the bridge; the Omega container holds no keys.

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

The split that keeps the money safe: language on one side, decisions and signatures on the other.
PartDoesNever
The language model, through Omega's providerReads messages, chooses which skill to call next, writes replies and explanations.Picks an amount, decides eligibility, or signs.
MeTTa rules, in OmegaDerives every amount, runs the nine checks, chooses the route, decides the next step after a failure.Talks to people.
Omega memoryRemembers requests, payouts and failures across sessions and events.Overrides the rule or the ledger.
Omega skillsThe 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.
BridgeSupplies 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 testnetEnforces two signatures, records receipts, holds the rule's hash.Knows the rule itself. It only sees the hash.
OrganiserAttests who qualified, approves or rejects each payout, signs in the browser.Pays alone.

The money gate

Every payout, whether Ahadi proposes it or a participant asks for it, becomes a set of facts and passes through 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.

omega/plugins/ahadi/ahadi.metta
; 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.

bridge/ahadi_bridge/main.py, abridged
# 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-published

    The rule's hash is recorded on chain.

  • rule-version

    The request names that exact rule.

  • eligibility-attested

    The organiser has said how many teams qualified.

  • place-exists

    The place is in the rule, and the wallet is the one registered for it.

  • place-amount

    The amount is exactly what the rule gives that place.

  • asset

    It pays in the rule's asset.

  • budget

    It fits in what is left of the award pool.

  • not-duplicate

    This exact request has not been seen before.

  • place-unpaid

    Nobody 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.

Checked in our build today
  • 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.

Signers and weights
  • Ahadi's key
    Lives in the bridge. Signs only what the rule allows.
    weight 1
  • Organiser's key
    Lives in the organiser's browser.
    weight 1
  • The fund's own key
    Switched off at setup.
    weight 0
Thresholds: weight needed for each kind of operation
2
Low
e.g. bump sequence
2
Medium
payments, claimable balances, data
2
High
signers and thresholds

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 passed

Each 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.

  1. test_vault_is_two_of_two

    Read back from Horizon: thresholds 2, 2, 2; the fund's own key weight 0; agent 1; organiser 1.

    Configured
  2. test_agent_alone_cannot_pay

    A payment signed only by Ahadi is refused, and the vault's sequence number does not move.

    tx_bad_auth
  3. test_organiser_alone_cannot_pay

    A payment signed only by the organiser is refused.

    tx_bad_auth
  4. test_disabled_master_cannot_pay

    The fund's own key, switched off, cannot pay either.

    tx_bad_auth
  5. test_both_signatures_pay

    With both signatures the payment settles, both signers are on the envelope, the memo is a hash, and exactly USD 2.50 arrives.

    Settled
  6. test_resubmitting_same_envelope_does_not_double_pay

    Sending the same signed envelope twice returns the same hash and moves the money once.

    Paid once
  7. test_unactivated_destination_fails_then_claimable_works

    Paying a wallet that was never activated fails; a claimable balance to the same wallet succeeds.

    op_no_destination, then held

A request's lifecycle

Every request ends on the ledger, whether it was paid, held, refused, rejected or recognised as a duplicate. The transaction is built and signed only when the organiser is ready, so a waiting proposal never expires.
  1. Requested
    A skill call or a message
  2. Checked by the rule
    MeTTa 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.
  3. Proposed
    Waits for the organiser
    • Rejected by the organiser. A rejected line with the organiser's reason.
  4. Agent signed
    prepare re-checks the rule, then signs
  5. Organiser signed
    In the organiser's browser
  6. Submitted
    Signed 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.
  7. Settled
    A paid line with its receipt

Paying exactly once

A payment system's worst bug is paying twice. Ahadi closes that door four times.
  1. 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. 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. 3

    The rule refuses repeats

    not-duplicate fails 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. 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.

Canonical request, October place 1 (captured 23 October)
{"amount_cents":17000,"asset":"USDB","event_id":"october-2026","place":1,"recipient":"GATT7DEYRM2GYSCKBUY4U22GKMS2I7EKWK3F4R356SS2CTXDNWQF4PHR","rule_hash":"4ea80d7c415bf35a0a9e4baa8647e15351bb85e56dece2cbf9273aec20bd9333","team":"Team Mvua"}
They match
sha256 of the request, computed for this page
d720ddf847393d19620c790ee822c5be93afb4abf37ae032549f974723516494

request id in the ledger, and memo on transaction ab4e8a83…5a76
d720ddf847393d19620c790ee822c5be93afb4abf37ae032549f974723516494
Check the memo on chain yourself: verify the transaction

When the network says no

A refusal from Stellar is never an error to hide. It becomes a failed line in plain words, nothing moves, and MeTTa decides the next step.
Result codeIn plain wordsWhat Ahadi does
op_no_destinationThis 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_trustThis 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_authOnly 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_lateThe 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_seqThe 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.

omega/plugins/ahadi/ahadi.metta
(= (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

Memory is for recall and explanation. The ledger and the chain are the truth, and the rule checks them, not memory.

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.