Deploy your first agent
By the end of this page you’ll have an XRPL agent that your Smart App owns. It has its own multisig wallet, it is bound to a rulebook published on-chain, and a trigger runs one of your app’s functions on a schedule to propose trades. Every proposal is checked against the rulebook before any validator signs.
Prerequisites
Section titled “Prerequisites”- An active Smart App, and a
BaasClientsigned in as it. The code below calls itbaas. - Your app’s XRPL wallet seed (
XRPL_SEEDin your env file). It signs the agent’s creation intent and pays the agent account’s reserve, so it must be the wallet your app signed in with. - SDK 13.0.1 or later:
@hsuite/smart-engines-sdk.
The flow
Section titled “The flow”
sequenceDiagram
participant You as Your app (owner + payer)
participant Host as Platform
participant Val as Validators
participant XRPL
You->>Host: prepareCreateAgentWithIntent (rule + signed intent)
Host->>Val: publish the rule, run the agent's DKG
Val-->>You: unsigned reserve payment + entityId
You->>You: check and sign the payment
You->>Host: executeCreateAgent (signed payment)
Val->>XRPL: submit the payment, set up the multisig account
Host-->>You: agent (active)
Note over Host,Val: later, on each trigger
Host->>Val: check each proposed action against the rulebook
Val-->>Host: signed, parked for your approval, or denied
Step 1 — Write the rulebook
Section titled “Step 1 — Write the rulebook”The rulebook is the agent’s authority. This one lets the agent place DEX offers and pay out only to you. Amounts are in drops (1 XRP = 1,000,000 drops):
agent-rule.ts import { Rules } from '@hsuite/smart-engines-sdk';const OWNER = 'rYourAppWalletAddress';export const agentRule = Rules.forAgent().withName('xrp-trader').withAgentType('trading').withActionTypeRestrictions({ allowed: ['offer', 'transfer'] }).withRecipientAllowlist({ addresses: [OWNER] }).withTradeLimits({ maxPerTrade: '25000000', dailyLimit: '50000000', periodMs: 86_400_000 }).withCooldown({ cooldownMs: 600_000 }).withApprovalThreshold({ threshold: '10000000', controller: 'owner' }).build();What it allows:
offeris what a DEX trade counts as. Leave it out and every trade is denied before any limit is checked.transfertoOWNERonly, so awithdrawcan bring funds back to you. Withdrawals pass the same rulebook.- Trades up to 25 XRP each and 50 XRP a day, at most one every 10 minutes. Anything above 10 XRP waits for your approval.
- No
approvedPairs. An offer that gives XRP is denied whileapprovedPairsis set (see Agent rules), so this XRP-selling agent is bounded bytradeLimitsinstead.
build()refuses a rulebook the platform would refuse at publish.Step 2 — Create the agent (payer-funded, two-phase)
Section titled “Step 2 — Create the agent (payer-funded, two-phase)”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. First the cluster publishes your rule, runs the agent’s DKG and hands you an unsigned payment for the reserve. Then you sign the payment and the cluster finalizes the account. See Identity & wallets.
create-agent.ts import { createXrplWeb3Signer, xrplWalletFundWith } from '@hsuite/smart-engines-sdk';import { agentRule } from './agent-rule';const signer = createXrplWeb3Signer(process.env.XRPL_SEED!);// Phase 1: publish the rule under your signed intent and run the agent's DKG.const prepared = await baas.entities.prepareCreateAgentWithIntent({primaryChain: 'xrpl',name: 'xrp-trader',agentType: 'trading',capabilities: ['function-invoke', 'trade', 'notify'],rulesConfig: {},},{signer: {chain: 'xrpl',walletAddress: signer.address,publicKey: signer.publicKey,signFn: signer.signFn,},rule: agentRule,},);if (!('prepared' in prepared)) throw new Error('expected the prepared reserve payment');// Check the reserve payment and sign it with your wallet. Nothing is submitted here.const funding = await xrplWalletFundWith(signer.wallet)(prepared.prepared, prepared);// Phase 2: the validators submit your signed payment and finalize the multisig account.const agent = await baas.entities.executeCreateAgent({entityId: prepared.entityId,chain: 'xrpl',signedFundingBlob: funding ? funding.signedFundingBlob : undefined,});if (!('agentId' in agent)) throw new Error('expected the created agent');console.log(agent.agentId, agent.status, agent.chainAccounts.xrpl); // "<agentId>" "active" "r…"Notes on this step:
capabilitieslists the verbs the agent may run, and nothing else will run. See Agent rules.xrplWalletFundWithrefuses to sign anything but the exact reserve payment the prepare described, from your wallet to the agent’s address, with a capped fee.- The agent is
activeas soon asexecuteCreateAgentreturns, because its account is already funded. - Agent ceilings. Each agent counts against your app’s agent limit (default 100), and an agent created for a customer also counts against that customer’s limit (default 25). Past a limit the create is refused with
400before the DKG runs. A deleted agent keeps its slot until its reserve is reclaimed.
Step 3 — Fund it
Section titled “Step 3 — Fund it”The creation payment covers only the account reserve. To give the agent XRP to trade with, send XRP from your wallet to the agent’s address (
agent.chainAccounts.xrpl), the same way you pay any XRPL account. Keep some XRP above the reserve: each open offer or trust line raises the account’s reserve.Check what it holds with
baas.agents.getBalances(agent.agentId).Step 4 — Give it triggers and run it autonomously
Section titled “Step 4 — Give it triggers and run it autonomously”An agent acts on triggers: a schedule (
intervalMs) or an event subject in your app’s namespace. Your app does not call the agent to make it act.The brain here is one of your app’s functions. Deploy it with
baas.functions.deployand note itsfunctionId. When the agent runs it, the function returns its decision and the actions it proposes asintents:// the value your function returnsreturn {decision: { reason: 'spread above target' },intents: [{capability: 'trade',chain: 'xrpl',params: {takerGets: { currency: 'XRP', value: '5000000' }, // gives 5 XRPtakerPays: { currency: 'USD', issuer: 'rUsdIssuer', value: '2.5' }, // wants 2.5 USD},idempotencyKey: 'trade:2026-12-01T10',},],};Then give the agent its rules config.
updateRulesreplaces the whole config and works only while the agent is paused:set-triggers.ts const appId = 'your-app-id';const functionId = 'your-function-id';const hook = 'https://hooks.example.com/agent';await baas.agents.pause(agent.agentId);await baas.agents.updateRules(agent.agentId, {functions: { allowedFunctionIds: [functionId] },offchain: { allowedUrls: [hook] },triggers: {schedule: [{ capability: 'function-invoke', intervalMs: 300_000, params: { appId, functionId, input: {} } },],events: [{ capability: 'notify', subject: `app.${appId}.billing.expired`, params: { url: hook, message: 'subscription expired' } },],},});await baas.agents.resume(agent.agentId);Schedule triggers are read on every poll of the trigger runner (every 30 seconds by default), so the new schedule applies at once. Event triggers are subscribed when the platform host starts, so a new or changed event trigger takes effect after the host’s next restart.
Every five minutes the agent runs your function. Each intent it returns is checked like a direct action: the capability must be declared (
tradeis), and the prepared offer must pass the rulebook before any validator signs. A proposal that breaks a limit is never signed. One above the approval threshold waits for your approval.validateAgentRules(exported by the SDK) runs the same checks on a rules config before you send it.updateRulesruns it for you.Beyond trading: the generic
Section titled “Beyond trading: the generic execute surface”executesurfacetradeis one capability. The agent runs any capability it declared:payout,token,notify,read,tradeandfunction-invoke. See Agent rules for their params.The SDK has
baas.agents.executeandbaas.agents.trade, but they accept only the agent’s own wallet as the caller, and nobody holds that wallet’s key. A call from your app or your owner wallet is refused with403. Drive the agent through triggers.A trigger cannot name a chain, so
trade,payoutandtokenfire from a trigger only as intents returned by a function, which carrychain.notifyandreadcan fire from a trigger directly.Let the agent fire itself (no scheduled function needed)
Section titled “Let the agent fire itself (no scheduled function needed)”For off-chain work you don’t need a function at all:
// on the agent's rulesConfig; appId is your Smart App's idtriggers: {schedule: [{ capability: 'read', intervalMs: 300_000, params: { url: 'https://api.example.com/signal' } }],events: [{ capability: 'notify', subject: `app.${appId}.deposit`, params: { url: 'https://hooks.example.com/x', message: 'deposit received' } }],}An event trigger’s subject must be in your own app’s namespace:
app.<appId>orapp.<appId>.<anything>. A subject that names a platform channel, a bare wildcard such as>orapp.*.>, or another app is refused when you set the rules and again when the agent subscribes. An agent can only be woken by its own app’s events. Every URL a trigger calls must be onoffchain.allowedUrls.Besides events your app publishes itself, the platform publishes these for your app:
Subject you can subscribe a trigger to Fires on app.<appId>.deploy.<name>your app’s deploy and runtime lifecycle ( created,activated,suspended,runtime.running, …)app.<appId>.billing.<expired|renewed|tier.changed>a subscription change on your app app.<appId>.baas.<insert|update|delete>a write to one of your app’s collections app.<appId>.messaging.publisheda message published on one of your app’s channels Each is derived from the source event’s own data, so a trigger only sees your app’s events. Triggers decide when; the rule check decides whether. See Lifecycle & autonomy.
Step 5 — Stay in control
Section titled “Step 5 — Stay in control”You want to… Call Stop it for now baas.agents.pause(agentId)Start it again baas.agents.resume(agentId)Change its rules config (paused only) baas.agents.updateRules(agentId, rulesConfig)Approve a parked operation (the answer carries its transaction) baas.agents.approve(agentId, operationId)Reject a parked operation baas.agents.reject(agentId, operationId)Move funds to an allow-listed address baas.agents.withdraw(agentId, { chain, amount, destination })Disable it permanently baas.agents.revoke(agentId)Delete it (it keeps its slot until the reserve is reclaimed) baas.agents.delete(agentId)Close a deleted agent’s XRPL account and return its balance to the funder baas.agents.reclaimReserve(agentId)Read its lifecycle log baas.agents.getEvents(agentId)Check its rule binding and recent activity baas.agents.certification(agentId)An operation above your
approvalThresholdis parked, the agent moves toawaiting_approval, and the operation waits for yourapproveorreject. See Lifecycle & autonomy and the audit trail.
What you just built
Section titled “What you just built”An agent with its own XRPL multisig wallet, bound to a rulebook published on-chain, that runs your function on a schedule. The function holds no keys, and every action it proposes is checked against the rulebook before any validator signs. You can pause it, change its triggers, approve large moves and shut it down.