Public
Rules are published to a Hedera Consensus Service (HCS) topic — a public, append-only log. Anyone can read the rules an entity is bound to.
A blockchain transaction is only half the story. The other half is: under what rules was this allowed to happen? On most platforms that policy lives in private backend code you can’t see and can’t audit. Smart Engines inverts that. The rules are public on chain, mandatory, and enforced at the one place that matters — the signing gate.
Public
Rules are published to a Hedera Consensus Service (HCS) topic — a public, append-only log. Anyone can read the rules an entity is bound to.
Mandatory
Enforcement is fail-closed at the signing gate. There is no “skip the rules” path — a missing reader, malformed memo, malformed rule, or rule mismatch all deny.
On-chain anchored
The governing rule is anchored to the entity’s immutable creation-transaction record on its own chain of origin. The validator’s local copy is treated as a cache, not the authority.
There is a single canonical enforcement point, the sign gate, and every chain’s signing path calls it before releasing a signature, a DKG share or an approval.
Because enforcement lives at the signing layer — not at an HTTP endpoint or in app code — it cannot be routed around. A validator simply will not contribute its threshold-signature share unless the gate says allow. And since signing requires a threshold quorum, a single misbehaving validator that ignores the rules can’t push anything through — the honest validators refuse, so the signature never reaches threshold.
%%{init: {'sequence':{'showSequenceNumbers':false}}}%%
sequenceDiagram
autonumber
participant Req as Transaction request
participant Gate as Sign gate (per validator)
participant Chain as Source chain (entity's creation tx)
participant HCS as Hedera HCS (canonical rules topic)
participant TSS as Threshold signer
Req->>Gate: share-request for entity E, op = transfer(...)
Gate->>Chain: read E's creation-tx memo (immutable)
Chain-->>Gate: memo → RuleRef (points at HCS topic + timestamp)
Gate->>HCS: resolve canonical rule
HCS-->>Gate: ValidatorRules (immutable, cached by consensus timestamp)
Gate->>Gate: verify resolved rule == the validator's cached copy
Gate->>Gate: classify op + evaluate rule
alt allow
Gate->>TSS: release partial signature share
else deny (any failure → fail-closed)
Gate-->>Req: { allowed: false } — no share released
end
The flow:
RuleRef (always pointing at Hedera HCS — that’s where the canonical rules topic lives).The canonical rules themselves live on Hedera HCS — that’s the one public log everyone reads. But the anchor (the immutable on-chain pointer from an entity back to its governing rule) is stamped on the entity’s own chain of origin, at creation. On every supported chain, an entity is born with a tamper-evident reference to the rule that governs it.
Each chain commits the anchor with its native immutability primitive:
| Chain | How the rule-anchor is committed at creation |
|---|---|
| Hedera Live | Immutable transaction memo on the create-account / create-token transaction (HCS topics carry it natively) |
| XRPL Live | rule: memo on the SignerListSet |
| Stellar Coming next | Memo.hash of the SHA-256 of the rule grammar (the full ref is committed as a digest — no truncation) |
| Polkadot Coming next | On-chain remark / memo alongside the multisig setup |
| Solana Coming next | Memo on the create transaction |
| Bitcoin Coming next | OP_RETURN on the funding transaction (UTXO model has no create-tx, so the anchor rides the first funding spend) |
The validator’s read side mirrors this: each chain has a reader that reads the anchor back off that chain, so the sign gate’s “re-derive from the birth certificate” check works identically no matter which ledger the entity lives on.
The validators fetch the canonical ValidatorRules for a RuleRef by its HCS consensus timestamp. Because HCS messages are immutable, a rule resolved for a given timestamp never changes — so it can be cached aggressively and the cache can never go stale.
Changing an entity’s policy therefore isn’t an in-place edit; it’s publishing a new immutable rule version to HCS. The old version remains in the public log forever. That’s what makes the policy history auditable: you can see every rule that was ever in effect, and exactly when.
Publishing a new rule version is one thing; deciding who is allowed to is another. Rule changes for an established entity don’t happen by operator fiat — they happen through a DAO governance vote. A passed dao_parameter_change proposal publishes the amended rule to HCS wrapped in an amendment envelope that names the proposal that authorized it and the exact field path it was permitted to change.
The sign gate never trusts that envelope at face value. It re-verifies the whole thing from on-chain facts before it will honor an amended rule.
The pointer is an untrusted hint. The entity’s current-rule pointer is replicated between validators; the gate treats it as a hint, not authority. To honor an amended head, the sign gate walks the chain backwards — amended head → … → the genesis rule anchored at the entity’s immutable creation-transaction memo — and proves every link before accepting any of it.
For each link, the gate requires:
Only if every link in the walk passes does the gate evaluate the amended rule. Any gap — missing envelope, wrong topic, content that doesn’t match the vote, a proposal that didn’t pass — fails closed, exactly like every other branch of the gate. Genesis rules stay published bare, and both shapes decode the same way.
The cost of writing each amendment to Hedera is paid by the protocol Treasury — a cluster-controlled DKG-multisig account — not by any single validator’s keys.
The gate is fail-closed at every branch and never throws — it returns { allowed: false } so callers deny deliberately rather than crashing into an ambiguous state:
POST /api/v3/baas/rules/simulateYou don’t have to broadcast a transaction to find out whether the rules would allow it. POST /api/v3/baas/rules/simulate (baas.rules.simulate in the SDK) evaluates a concrete action against a rule and returns a truthful accept / reject + reason — no signature, no chain write.
Crucially, the endpoint runs the same evaluator the validator’s sign gate uses against the real production predicates — maxPerTransaction, allowlist / blocklist, timeRange, and the rolling period caps (daily / weekly / monthly / total) when you supply the window totals. It is not a shape-check that rubber-stamps any schema-valid rule; it answers “would this 500-unit transfer be rejected by this account’s rule?” for real.
You can pass either a ruleRef (resolved from HCS) or an inline rule, plus the action and context:
%%{init: {'sequence':{'showSequenceNumbers':false}}}%%
sequenceDiagram
autonumber
participant SDK as SDK / smart-app
participant Sim as "POST /api/v3/baas/rules/simulate"
participant Eval as "Sign-gate evaluator"
SDK->>Sim: "{ ruleRef | rule, action, context }"
Sim->>Sim: resolve canonical ValidatorRules
Sim->>Eval: classify op + evaluate against real predicates
Eval-->>Sim: allow / deny + reason
Sim-->>SDK: "{ isValid, reason }"
Like the gate, simulate is fail-closed: an unresolvable ref, a malformed rule, an unrecognised action, or any evaluation error returns { isValid: false, reason } — it never throws and never accidentally returns allow. (Period-capped rules deny a dry-run that omits context.windowSums, since you can’t claim “within the daily cap” without knowing the day’s spend.)
What gets published to HCS is a ValidatorRules document: a typed set of canonical fields, each of which the sign gate knows how to enforce. You build one with the SDK’s rule builders (Rules.forToken, Rules.forAccount, Rules.forTopic, Rules.forAgent) and can check it locally with Rules.validate before you publish.
Limits and windows
Per-transaction maximums, rolling daily / weekly / monthly / total caps, and time ranges.
Who and what
Allowlists and blocklists, recipient allowlists, and the action types an entity may perform.
Token behaviour
Token controls such as soulbound transfer restrictions, declared as fields the gate evaluates.
Each field is a named check. When a rule check refuses a request with a 403 rule_rejected response, the response names the checks that refused it in ruleAtoms, alongside a reason. A rule that carries modules — a composition format the sign gate does not enforce — is refused at publish and denied at the gate, so every control must be declared as a canonical field.
A couple of checks worth calling out for security:
soulbound.enforced denies every transfer of the token decided under that rule — the token entity’s own transfers. Another holder’s transfer moves under that holder’s own rule, or under none outside the platform, so it does not stop a holder from transferring the NFT. Mint, burn and wipe are decided by their own operations.Smart-agents (autonomous on-chain actors) are first-class canonical entities — they get their own DKG-multisig identity and their own canonical rules, gated by the same sign gate as any user account. An agent’s rule (type: 'agent') states exactly what actions the agent is permitted to take. There is no privileged bypass for automation.
The agent path is default-deny. When an agent is registered with no explicit rules, the default rule (buildDefaultAgentValidatorRules in the SDK) is inert on-chain, not merely amount-capped:
actionTypeRestrictions permits only transfer — mint, burn, and offer are denied outright by action-type.recipientAllowlist permits only a sentinel address (__deny_all__) that no real chain account ever equals — so every transfer is denied too.A freshly-registered agent can therefore move nothing until its owner configures a real recipient/action allow-list (or supplies explicit ValidatorRules at register time, which short-circuits the default). Deny-all is expressed within the non-empty-array schema — “allow only a value nothing matches” — and the sign gate enforces it like any other predicate.
The evaluator itself is default-deny on the agent arm, independently of the document: an agent rule that omits actionTypeRestrictions permits no operation, and one that omits recipientAllowlist permits no transfer destination. Leaving a field out withholds authority; it never grants it. An app may not publish (or bind an agent to) an agent rule that grants nothing this way: one with no actionTypeRestrictions, or one allowing transfer with no recipientAllowlist, is refused at publish, inline simulate, SDK build and agent creation.
A scripting engine (Lua, Starlark) is powerful but opens an attack surface, defeats static analysis, and makes hot-path performance unpredictable. Freeform JSON is unstructured and unauditable. Hard-coded TypeScript means every new product is a code change. A typed set of canonical fields is the deliberate sweet spot: declarative enough to publish on chain, typed enough to validate, fast enough for the signing hot path, and small enough that every field is enforced by the gate.
POST /api/v3/baas/rules/simulate runs the same evaluator as the sign gate for a real, fail-closed dry-run accept/reject — no signature, no chain write.ValidatorRules document; modules are refused.