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.
Three wallets, three roles
Section titled “Three wallets, three roles”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.
Per-entity DKG: no single signer, ever
Section titled “Per-entity DKG: no single signer, ever”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.
Payer-funded, two-phase create
Section titled “Payer-funded, two-phase create”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:
- 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’sentityId, its address, the reserve, and the unsigned reserve payment. (prepareCreateAgentdoes the same when you already hold a publishedruleRefand a signed intent.) - Sign. Your wallet signs the payment. By default the payer is your app’s session wallet; pass
payerAccountIdto name another payer you control.xrplWalletFundWith(wallet)checks the payment is exactly the reserve to the agent’s address before signing. - 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, omitsignedFundingBloband call this once the payment is validated; until then it answers409, which you can retry. The agent comes back with itsagentId, alreadyactive.
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.
Reclaiming a deleted agent’s reserve
Section titled “Reclaiming a deleted agent’s reserve”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
409andretryLater: false. Clear them first. - A sweep already in flight, or an account too young to delete under XRPL’s 256-ledger rule, answers
409withretryLater: true. Try again shortly. - A live, undeleted agent answers
404.
Multiple chains
Section titled “Multiple chains”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.