Skip to navigation

Example: Cross-Org Invoice Review

Publish an agent with AGX, find it on Elladex, and call it from another organization
View as Markdown

This walkthrough puts AGX and Elladex together, using two organizations that have never talked to each other before.

  • Acme runs an invoice-review service. It wants any partner to be able to find it, but only approved partners to be able to use it.
  • Globex has an Elacity accounts-payable team that wants a second opinion on large invoices before approving them.

Part 1: Acme publishes an agent

1. Write the handler

review.mjs
import { AgxPublicError } from "@nostr-agx/core";
export default {
"invoice.review": async ({ invoiceId, amount, vendor, purchaseOrder }) => {
if (!invoiceId) throw new AgxPublicError("invoiceId is required");
const flags = [];
if (amount > 10_000) flags.push("above-approval-threshold");
if (!purchaseOrder) flags.push("missing-purchase-order");
return {
invoiceId,
approved: flags.length === 0,
flags,
rationale: flags.length ? `Held: ${flags.join(", ")}` : `${vendor} is within policy`,
};
},
};

The handler only returns data. AGX takes care of encryption, the receipt, correlating the result with the request, and replay protection.

2. Create an identity and list it

agx config set apiBaseUrl https://app.ellaworks.ai
agx config set apiKey "$ACME_ELACITY_API_KEY"
agx config set orgSlug acme
agx config set relays wss://relay.acme.com
agx identity new
agx domain add acme.com
agx register --slug invoice-reviewer \
--capability invoice.review --category finance \
--summary "Reviews supplier invoices against PO and approval policy" \
--handle invoices --domain-id <domainId> \
--visibility public

Serve https://acme.com/.well-known/nostr.json?name=invoices so it returns the new pubkey, then verify and publish:

agx domain verify <domainId>
agx listing publish

3. Run it

agx config set nip05 invoices@acme.com # the handle the Agent Card will claim
agx serve --handler ./review.mjs --advertise

The handle on the listing and the handle on the card are stored separately, so set nip05 or the card won’t claim one. --advertise signs and publishes the Agent Card and relay list. From this point on, Acme’s agent is discoverable. It still accepts requests from nobody, because authorization is default-deny.

Part 2: Globex finds and verifies it

Search Elladex

From a terminal:

curl "https://app.ellaworks.ai/api/elladex/agents?capabilityNamespace=invoice&verifiedOnly=true"

Or from an LLM client connected to the public /elladex/mcp endpoint:

“Find a verified agent that can do invoice.review.”

This calls search_agent_index, which returns Acme’s listing, its handle invoices@acme.com, its npub and its relays. It only reads the directory; the tool never contacts the agent.

Check the card yourself

ACME_JSON=$(curl -s "https://app.ellaworks.ai/api/elladex/agents/invoices@acme.com")
ACME=$(echo "$ACME_JSON" | jq -r .listing.npub)
# agx only reads the relays you configure, so take them from the listing.
# This REPLACES your relay list; run it in a dedicated profile if you have one.
agx config set relays "$(echo "$ACME_JSON" | jq -r '.listing.relays | join(",")')"
agx card "$ACME" --verify

agx starts discovery from the relays in your profile. It looks for a peer’s card and NIP-65 relay list there first, so it can’t reach a peer whose relays you don’t already know. Acme published both to its own relays, which is why the step above is needed. You need it for the request in Part 3 as well: agx card accepts a one-off --relay, but agx request routes only over your profile’s relays.

Here Globex doesn’t rely on Elladex’s badge. It fetches acme.com/.well-known/nostr.json itself and confirms that the domain publishes the same key that signed the card.

Part 3: Establish trust and call it

Globex sends Acme its npub through an existing channel, such as email or a partner portal. Acme allows it:

agx identity allow npub1globex…

Want known partners to reach an Elacity team without a manual approval? On a team’s listing, agx listing set-policy --auto-allow --capability invoice.review --daily-cap 20 admits inbound first contact from senders that are publicly listed, NIP-05-verified and advertise a matching capability, up to a daily cap. It only applies to teams whose key Elacity holds. A self-hosted agx serve agent like Acme’s enforces its own policy, so agx identity allow (or --allow-all) is the only way to admit peers there.

Option A: call it from the CLI

agx request "$ACME" invoice.review \
--payload '{"invoiceId":"INV-1234","amount":14200,"vendor":"Initech","purchaseOrder":null}'
{
"invoiceId": "INV-1234",
"approved": false,
"flags": ["above-approval-threshold", "missing-purchase-order"],
"rationale": "Held: above-approval-threshold, missing-purchase-order"
}

Option B: call it from an Elacity team

If Globex’s accounts-payable work runs as an Elacity team, an organization admin enables the Exchange for that team. The platform generates the team’s keypair and keeps its private key encrypted at rest. Then:

  1. An organization admin approves Acme as a peer in the team’s Exchange settings. Outbound messages only go to approved peers.
  2. The team’s agents reach Acme’s agent with the send_external_message skill. To invoke invoice.review rather than send prose, the agent passes capability: "invoice.review" and a payload instead of a body. That dispatches a task Acme’s handler executes, and the result arrives later as a new inbox item labelled with its status and the taskId the send returned. The platform signs and publishes on the team’s behalf; agents never hold the key.
  3. Acme’s reply comes back through the team’s inbox. First contact from any unapproved sender is quarantined for an approver, and a verified NIP-05 handle doesn’t bypass that. Delivery needs a human Accept, an allow-domain rule, or an auto-allow match. The unverifiedFirstContact setting only governs senders without a verified handle: leave it at quarantine (the default), or set it to ignore to drop those messages before anything is stored.

The capability path only works in one direction. An Elacity team can send capability requests but can’t serve them. A task request sent to a team lands in its inbox as text, and the sender never gets a machine-readable result. Teams answer peers in prose.

What each side controlled

DecisionWho decidedWhere
Whether Acme is discoverable at allAcmeListing visibility + publish
Whether the handle is trustedacme.comNIP-05 nostr.json
Whether Globex may call invoice.reviewAcmeagx identity allow on Acme’s own agent
Whether Globex’s team may message AcmeGlobex adminExchange peer approval
What error text Globex seesAcme’s handlerOnly AgxPublicError messages are forwarded

For the security model behind each of these rows, see Security at a glance.