Skip to content

Identity & wallets

You can let an automated actor propose real transactions because nobody holds a key that can move the agent’s money alone. The agent’s wallet is a multisig whose key is created by a DKG ceremony across the validators and exists only as shares. To sign, each validator makes a signature share with its own key share, and the shares are combined into one signature. The whole key is never assembled, not at rest and not while signing. The agent proposes; the validators sign only if the rules pass.


Every agent record carries three wallet roles:

Role Field Can do
Owner ownerWallet Everything else: fund, withdraw, pause, resume, revoke, delete, updateRules, approve, reject, reclaimReserve. The person or app in charge.
Agent agentWallet Run the agent’s declared capabilities, only within the rules. The only caller a capability run accepts.
Treasury treasuryAccount Holds the funds. It is the DKG multisig account and initiates nothing itself.

On an XRPL agent, the agent wallet and the treasury are the same multisig account, and no person holds its key, so the agent acts only through its triggers. Owner-only calls are refused for any other wallet. The caller’s wallet comes from the authenticated session, never from the request body.

Per-user ownership: the app acts on a customer’s behalf

Section titled “Per-user ownership: the app acts on a customer’s behalf”

When a Smart App runs agents for its own users, the app is the authenticated caller, but each agent’s owner should be the customer, not the app. The app names the customer by forwarding the customer’s session in the X-Customer-Session-Claim header; baas.forCustomer(token) sends it for you on agent creates and every agent write. With a valid claim, the customer is the owner. A claim that is present but invalid or expired is refused with 401; it never falls back to the app.

Three more rules apply to a customer’s agent:

  • The customer signs the creation intent. It is signed by the customer’s own wallet in your frontend (signEntityCreateIntent), over the exact rule, which is then published under that intent. The SDK refuses to sign a customer’s intent with the app’s own signer.
  • The app sponsors the cost. Publishing the rule and preparing the agent are platform-paid, so for a customer the call needs { sponsorCustomerWrite: true }. Without it the call is refused, and the SDK refuses it before sending.
  • Per-customer limits apply. A sponsored create counts against the customer’s agent limit as well as the app’s.

The same identity bounds what can be read. Every agent read is scoped to the session’s tenant and to one owner: get, list, events, balances and certification. A forwarded customer sees only its own agents; the app acting as itself sees only the agents it owns. A list filter naming anyone but the caller (?owner=) returns an empty list. Another tenant’s or another owner’s agent returns the same 404 as an unknown id, and so do the owner routes (fund, withdraw, pause, …). A claim minted for a different app is refused with 403.

customerScope: 'strict', the default for every app, removes the remaining trust in the app. Under strict, a request on the agents plane that carries no customer identity is refused with 403 before any lookup. Agent creates and the owner routes read only the claim, so an X-Customer-Session-Token there is refused (CUSTOMER_IDENTITY_NOT_VALIDATED_ON_ROUTE); agent reads accept either header. If your app forgets to forward a customer on one call, that call fails instead of running as the app. Under strict the app cannot create or read an agent it owns itself; for that it needs 'permissive'. See TokenGate.


Agents are canonical entities, created through the same per-entity path as every other entity: a Distributed Key Generation ceremony across the active validators that produces the agent’s own key, held only as shares, and a native multisig account on its chain. A threshold of validators must each sign with their own share for any transaction to be valid.

An XRPL account must hold a reserve to exist, and the platform never pays it for you. So an XRPL agent is created in two phases, like an XRPL account entity:

  1. Prepare. baas.entities.prepareCreateAgentWithIntent(req, { signer, rule }) publishes your rule under your signed creation intent, then runs the agent’s DKG. Nothing is submitted on-chain yet. It returns the agent’s entityId, its address, the reserve, and the unsigned reserve payment. (prepareCreateAgent does the same when you already hold a published ruleRef and a signed intent.)
  2. Sign. Your wallet signs the payment. By default the payer is your app’s session wallet; pass payerAccountId to name another payer you control. xrplWalletFundWith(wallet) checks the payment is exactly the reserve to the agent’s address before signing.
  3. Execute. baas.entities.executeCreateAgent({ entityId, chain: 'xrpl', signedFundingBlob }) has the validators submit your signed payment and finalize the multisig account. If you submitted the payment yourself, omit signedFundingBlob and call this once the payment is validated; until then it answers 409, which you can retry. The agent comes back with its agentId, already active.
sequenceDiagram
    participant You as You (owner + payer)
    participant Host as Platform
    participant Val as Validators
    participant Chain as XRPL

    You->>Host: prepareCreateAgentWithIntent (rule + signed intent)
    Host->>Val: publish the rule, run the agent's DKG (nothing on-chain)
    Val-->>You: unsigned reserve payment + entityId
    You->>You: check and sign the payment
    You->>Host: executeCreateAgent (signedFundingBlob)
    Val->>Chain: submit the payment, finalize the multisig
    Host-->>You: agent (agentId, active)

The two-phase create serves XRPL only. The synchronous create (baas.entities.createAgent, baas.agents.register) serves Polkadot, Solana and Bitcoin, which are not live yet, and refuses XRPL with 400.

The reserve you paid to open the agent’s XRPL account comes back when you’re done with the agent. After you delete the agent (which revokes and hides it), baas.agents.reclaimReserve(agentId) closes the account with an XRPL AccountDelete and sends its whole balance to whoever funded it. It is owner-only, and the caller doesn’t choose the destination: the validators read the funder from the account’s creating payment on the ledger, so no request can redirect the money. The AccountDelete is signed by the agent’s multisig through the same rule check as every other agent move.

The agent’s slot in your agent limit is released once, and only after the ledger confirms the account is gone, never on the API call alone. What to expect:

  • An account that holds objects that block deletion (trust lines, escrows, checks, payment channels, NFTs) is refused with 409 and retryLater: false. Clear them first.
  • A sweep already in flight, or an account too young to delete under XRPL’s 256-ledger rule, answers 409 with retryLater: true. Try again shortly.
  • A live, undeleted agent answers 404.

An agent has a primaryChain, where its multisig is created, and optional additionalChains. Agent creation per chain:

Chain Status How an agent is created
XRPL Live Two-phase, payer-funded create
Polkadot Coming next Synchronous create
Solana Coming next Synchronous create
Bitcoin Coming next Synchronous create
Ethereum, Polygon Postponed Not created today

Hedera is used for Hedera Consensus Service, where agent rules are published. Agents are not created on Hedera.