Skip to content

Agent rules

The rules are what let you hand a wallet to an automated actor. An agent’s rulebook is a use of the platform rules engine, the same engine that governs every other entity, with predicates suited to an actor that spends money: trade limits, cooldowns, allocation caps, approval thresholds, operating windows and approved pairs. The rulebook is published on Hedera Consensus Service before the agent is created, and the agent is bound to it.


You build an agent rulebook with the SDK’s Rules.forAgent() builder. It produces the canonical agent rule (type: 'agent'). This one is for an agent that trades XRP on the XRPL DEX and may pay out only to its owner:

agent-rule.ts
import { Rules } from '@hsuite/smart-engines-sdk';
const OWNER = 'rYourOwnerAddress';
export const agentRule = Rules.forAgent()
.withName('xrp-trader')
.withAgentType('trading')
// Authority: DEX offers, and transfers to the owner only.
.withActionTypeRestrictions({ allowed: ['offer', 'transfer'] })
.withRecipientAllowlist({ addresses: [OWNER] })
// Amounts are in the chain's smallest unit: drops on XRPL (1 XRP = 1,000,000 drops).
.withTradeLimits({ maxPerTrade: '25000000', dailyLimit: '50000000', periodMs: 86_400_000 })
.withCooldown({ cooldownMs: 600_000 }) // 10 minutes between moves
.withApprovalThreshold({ threshold: '10000000', controller: 'owner' }) // above 10 XRP, the owner approves
.build();

The builder defaults the rule to defaultSecurity: 'full' and defaultController: 'owner'.

The predicates an agent rule can use are tradeLimits, cooldown, allocationLimits, approvalThreshold, operationalWindow, approvedPairs, recipientAllowlist, actionTypeRestrictions and permissions, plus the rule-level timeRange, tokenGates and invariants that every rule shares. There is no stopLoss field. You contain losses with trade limits, allocation limits and the daily cap.

A few things to know when you write one:

  • Units. Amounts are compared in the chain’s smallest unit, such as drops on XRPL.
  • Action types. actionTypeRestrictions.allowed is the coarse authority gate. An agent rule can grant transfer, mint, burn and offer. A DEX trade on XRPL is an OfferCreate, which counts as offer, so a trading agent must list offer, or every trade is denied before any limit is checked.
  • Offers have no recipient. recipientAllowlist does not apply to an offer. It still applies to every transfer. An offer’s spend is bounded by tradeLimits, approvedPairs, cooldown, operationalWindow and approvalThreshold. The amount checked is what the agent gives (the offer’s TakerGets).
  • Approved pairs and XRP. approvedPairs matches the token the agent gives. An offer that gives XRP has no token id, so it is denied while approvedPairs is set. For an agent that sells XRP, leave approvedPairs out and bound it with tradeLimits.
  • Operating window. operationalWindow is a bounded period (from and to, as unix seconds or a date string), not a recurring schedule. Daily opening hours cannot be expressed.
  • Approval controller. An approvalThreshold names owner or dao as the controller.

A value the gate cannot enforce is refused when you publish, not ignored:

  • a permissions.mode other than whitelist, wildcards, self-add, or an entry scope below execute
  • a cooldown.perAction key that names no agent action
  • a tradeLimits.periodMs other than one day
  • decimals other than 0
  • approvedPairs.strictMode: false
  • approvalThreshold.currency
  • an operationalWindow.timezone other than UTC, or inclusive: false
  • allocationLimits.min, or an allocationLimits.max without its tokenId

baas.rules.simulate reports each of these as a schema error on its own path. A permissions entry’s expiresAt is enforced, and a cooldown.perAction entry overrides cooldownMs for that action. An approvedPairs pair applies on its own chain only, so the same token id on another chain is denied.

Default-deny: an unconfigured agent is inert

Section titled “Default-deny: an unconfigured agent is inert”

If you create an agent without supplying a rule, the platform uses its default agent rule (buildDefaultAgentValidatorRules() in the SDK), and that default denies everything. The agent can move nothing on-chain until you grant it something:

  • actionTypeRestrictions: { allowed: ['transfer'] }, so mint, burn and offer are denied by action type.
  • recipientAllowlist: { addresses: ['__deny_all__'] }, a placeholder no real account matches, so every transfer is denied too.

The schema requires non-empty lists, so “deny all” is written as “allow only a value nothing matches”.

Leaving a gate out withholds authority. It never grants it. An agent rule with no actionTypeRestrictions permits no on-chain operation at all, and one with no recipientAllowlist permits no transfer destination. Simulating such a rule reports actionTypeRestrictions absent — … (default-deny) for every action.

Because a rule like that looks unrestricted while granting nothing, it is refused rather than published. rules.publish, rules.simulate on an inline rule, the SDK’s forAgent().build() and Rules.validate, and agent creation all reject an agent rule that has no actionTypeRestrictions (or an empty one), or that allows transfer without a recipientAllowlist. Only transfer needs the allow-list: { allowed: ['offer'] } or { allowed: ['mint'] } without a recipientAllowlist is accepted, because neither sends value to a recipient.


Trading is one thing an agent can do. An agent declares a list of capabilities when it is created, and every action it takes runs one of them. Six capabilities exist:

Capability What it does Params
trade Places a DEX offer from the agent’s wallet (an XRPL OfferCreate) { takerGets, takerPays }, each { currency, value, issuer? } (issuer required for anything but XRP; XRP values in drops)
payout Transfers from the agent’s wallet { destination, amount, tokenId? }
token Mints, burns or transfers a token { op, tokenId, amount, to? }
notify Sends an outbound POST { url, payload? or message?, headers? }
read Sends an outbound GET { url, headers? }
function-invoke Runs one of your app’s functions as the agent { appId, functionId, input? }

Each capability turns into one or more steps:

  • On-chain steps (trade, payout, token) are prepared from the agent’s wallet and checked against the rulebook before any validator signs. They also pick up approval thresholds.
  • Off-chain steps (notify, read) reach only URLs on the agent’s rulesConfig.offchain.allowedUrls. Any other URL is refused before the request leaves.
  • Function steps run only functions on rulesConfig.functions.allowedFunctionIds, and only in the app the agent belongs to.

An agent can only run a capability it declared. An undeclared one is refused before anything is prepared.

Running smart-functions as itself — metered, quota’d, attested

Section titled “Running smart-functions as itself — metered, quota’d, attested”

A function-invoke step runs one of your app’s functions under the agent’s own identity rather than the app’s, so its usage is attributed to the agent. The function holds no keys. If it returns a decision with a list of intents, each intent is dispatched as one of the agent’s capabilities and checked exactly like a direct one:

// what a function returns to propose actions
return {
decision: { reason: 'spread above target' },
intents: [
{
capability: 'trade',
chain: 'xrpl',
params: {
takerGets: { currency: 'XRP', value: '5000000' }, // 5 XRP, in drops
takerPays: { currency: 'USD', issuer: 'rUsdIssuer', value: '2.5' },
},
idempotencyKey: 'trade:2026-12-01T10',
},
],
};

A function can return at most eight intents. Each intent’s capability must be one the agent declared.

Two daily caps apply per agent: the number of function invocations (default 1,000 a day) and their estimated cost, priced against the app’s tier (default $100 a day). The agent has no tier of its own, so an agent cannot drain the app’s subscription with expensive calls. The cost cap is a cost control, not a security boundary: if a price can’t be computed, the call is allowed.


The rulebook is checked in two places, and both fail closed.

When you publish the rule

A rule that grants nothing, or uses a value the gate cannot enforce, is refused at publish and at agent creation. You find out before the agent exists, not when it first tries to act.

Before any signature

Every on-chain move the agent proposes is prepared as a transaction and checked against the bound rulebook before any validator signs. If the check denies it, no signature is produced.

sequenceDiagram
    participant Agent as Agent (proposer)
    participant Host as Platform
    participant Gate as Rule check (fail-closed)
    participant Val as Validators

    Agent->>Host: run a capability (e.g. trade)
    Host->>Host: agent active? capability declared?
    Host->>Gate: check the prepared transaction
    alt allowed
        Gate->>Val: sign (threshold of validators)
        Val-->>Host: signed transaction
    else above the approval threshold
        Gate-->>Host: parked for the owner
        Host-->>Agent: awaiting approval
    else any other denial
        Gate-->>Host: denied, nothing signed
        Host-->>Agent: refused
    end

A denial for the approval threshold is not a rejection. The operation is stored as pending, the agent moves to awaiting_approval, and the owner calls approve or reject. Any other denial is final.


If the rule check cannot run, the operation is denied rather than let through. The same holds for transaction preparation and for storing a pending approval: a missing dependency means no operation, never an unchecked operation.


What you can inspect today:

  • The rulebook. It is published on Hedera Consensus Service before the agent is created, and the agent records its ruleRef (where it was published) and rulesHash. Anyone can check which rule governs the agent.
  • The event log. baas.agents.getEvents(agentId) returns the agent’s lifecycle record, newest first: its creation, every status change (including an operation parked for approval), rule changes, and the operations you approved or rejected.

Anchoring each agent’s state history on Hedera Consensus Service is built, not yet enabled.

Verifying an agent: the certification surface

Section titled “Verifying an agent: the certification surface”

baas.agents.certification(agentId) (GET /baas/agents/:agentId/certification) returns the agent’s certification snapshot in one read:

  • its entityId, rulesHash and ruleRef
  • its owner, agent and treasury addresses and its status
  • its recent event-log entries and the transaction ids they carry

The snapshot holds addresses and the rules hash, so it is not public. It is scoped like every other agent read: you see an agent in your own tenant that you own. When a Smart App acts for a customer, it sees only that customer’s agents; when it acts as itself, only the agents it owns. An agent outside your scope returns the same 404 as an id that doesn’t exist. See Identity & wallets.


baas.agents.updateRules(agentId, rulesConfig) replaces the agent’s rules config: its trade market, its off-chain and function allow-lists, and its triggers. It works only while the agent is paused, which forces a stop-edit-resume cycle instead of a hot swap. The change is recorded in the event log. Event-trigger subjects are checked again on every update.

updateRules does not change the rulebook the agent was created with. The limits and authority gates on this page stay bound to the agent’s ruleRef.