Skip to content

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.


  • An active Smart App, and a BaasClient signed in as it. The code below calls it baas.
  • Your app’s XRPL wallet seed (XRPL_SEED in 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.

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

  1. 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:

    • offer is what a DEX trade counts as. Leave it out and every trade is denied before any limit is checked.
    • transfer to OWNER only, so a withdraw can 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 while approvedPairs is set (see Agent rules), so this XRP-selling agent is bounded by tradeLimits instead.

    build() refuses a rulebook the platform would refuse at publish.

  2. 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:

    • capabilities lists the verbs the agent may run, and nothing else will run. See Agent rules.
    • xrplWalletFundWith refuses 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 active as soon as executeCreateAgent returns, 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 400 before the DKG runs. A deleted agent keeps its slot until its reserve is reclaimed.
  3. 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).

  4. 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.deploy and note its functionId. When the agent runs it, the function returns its decision and the actions it proposes as intents:

    // the value your function returns
    return {
    decision: { reason: 'spread above target' },
    intents: [
    {
    capability: 'trade',
    chain: 'xrpl',
    params: {
    takerGets: { currency: 'XRP', value: '5000000' }, // gives 5 XRP
    takerPays: { currency: 'USD', issuer: 'rUsdIssuer', value: '2.5' }, // wants 2.5 USD
    },
    idempotencyKey: 'trade:2026-12-01T10',
    },
    ],
    };

    Then give the agent its rules config. updateRules replaces 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 (trade is), 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. updateRules runs it for you.

    Beyond trading: the generic execute surface

    Section titled “Beyond trading: the generic execute surface”

    trade is one capability. The agent runs any capability it declared: payout, token, notify, read, trade and function-invoke. See Agent rules for their params.

    The SDK has baas.agents.execute and baas.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 with 403. Drive the agent through triggers.

    A trigger cannot name a chain, so trade, payout and token fire from a trigger only as intents returned by a function, which carry chain. notify and read can 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 id
    triggers: {
    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> or app.<appId>.<anything>. A subject that names a platform channel, a bare wildcard such as > or app.*.>, 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 on offchain.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.published a 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.

  5. 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 approvalThreshold is parked, the agent moves to awaiting_approval, and the operation waits for your approve or reject. See Lifecycle & autonomy and the audit trail.


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.