Security Model
This page states the custody and enforcement guarantees that make Tidal safe to integrate — the reasoning you would want before handing a system a signing key. It is grounded in Tidal's custody design and architecture direction. The design was arrived at adversarially: for each choice, an appealing alternative was rejected for a concrete reason, and those reasons are the model.
The core stance: a budget account, not a delegated key
The rejected alternative is the obvious one — give the agent a restricted key on the user's own
account. It fails because the XRP Ledger has no native per-key spending limit: a key that can sign a
Payment can sign away the whole balance. Under delegation, the blast radius of any policy failure,
compromise, or collusion is the user's entire account.
So every agent gets its own funded budget account, and the funded balance is a hard, on-ledger, trust-nobody cap on worst-case loss. Pre-funding, reserves, and manual top-ups are the price of that guarantee, and it is worth paying, because it replaces a promise with a property. (Auto-top-up would quietly rebuild unbounded exposure; if it is ever added it needs its own cap and its own revocation.)
Three keys, three authorities
Authority is split across three keys held by three parties. This separation is the spine of the model.
| Key | Held by | Signs | Cannot |
|---|---|---|---|
| Master | The user, in their Tidal wallet (encrypted at rest) | Structural transactions: funding, TrustSet, SignerListSet, emergency sweep | Never signs an agent's day-to-day payment |
| Agent | You, the developer, in your KMS — private half never leaves | The first signature on a payment | Cannot meet quorum alone; cannot change its own limits |
| Co-signer | Tidal's co-signer service, in HSM/KMS | The second signature, only after policy approval | Cannot meet quorum alone; can never touch the master |
The budget account carries a SignerList of {agent key, co-signer key} with quorum 2, and the
master key stays enabled and with the user. That the user can bypass the co-signer on their own
account is not a hole — it is self-custody. The gate exists to bound the agent's autonomous
spending, not to bound the owner of the money.
flowchart TB
subgraph acct["Budget account — SignerList quorum 2, master enabled"]
direction LR
q["Needs BOTH signatures to pay"]
end
master["Master key<br/>(user)"] -->|"structure + emergency sweep"| acct
master -.->|"can sweep even if Tidal is offline"| exit(["Direct XRPL exit"])
agent["Agent key<br/>(developer KMS)"] -->|"signature 1"| acct
cosigner["Co-signer key<br/>(Tidal HSM/KMS)"] -->|"signature 2, after approval"| acct
policy["Policy & risk"] -->|"approve → co-signer may sign"| cosigner
agent -- "alone: tefBAD_QUORUM" --x acct
cosigner -- "alone: tefBAD_QUORUM" --x acct
classDef u fill:#fff4e6,stroke:#d98a3a;
classDef d fill:#e8f4ff,stroke:#4a90d9;
classDef t fill:#e8fff0,stroke:#3aa76d;
class master u;
class agent d;
class cosigner,policy t;The attack table
Stating the guarantees as an adversary would test them:
- Agent alone — cannot produce a valid transaction. One signature does not meet quorum; the
ledger returns
tefBAD_QUORUM. Ledger-enforced. - Co-signer alone, or compromised — same. It holds one of two signatures and can never reach quorum. Ledger-enforced.
- Agent and co-signer colluding or both compromised — worst-case loss is the budget account balance, and nothing more, because the account is walled off from the user's main funds. Ledger-enforced.
- Policy caps — finer constraints inside those hard walls; service-enforced and auditable.
- Tidal unreachable or dead — the user's master key sweeps the account directly through a public XRPL endpoint. Ledger-enforced emergency exit.
The security improvement over the rejected sole-custody design is exact: compromising the policy authority yields no signing key, because it holds neither half of the quorum.
Key and authority separation, in the SDK
The separation is enforced in code, not just described. The agent key is generated and held only in
your key store; the SDK asks it to sign an exact prepared preimage and validates the contribution it
returns, but never sees the private key. Tidal never generates, imports, escrows, backs up, or
operates an agent private key. And keys are single-role: the agent key is never reused as an API
request-signing key, a webhook key, or a policy key. The SDK's packaging enforces the runtime
boundary too — the browser, extension, and React Native bundles contain no request-signing key
parser, no apiSecret handling, no KMS SDK, and no server implementation, and a packaging test
fails if a server import leaks into the public graph.
How limits are actually enforced
This is the honest boundary, and it bears repeating because it is where wallets tend to overclaim. Policy caps — per-payment, daily, lifetime, allowed assets and destinations — are enforced by the policy authority refusing to approve, which makes the co-signer withhold its signature. They are real and auditable, but they are service-enforced.
What the ledger enforces on its own, trusting no service, is exactly two things: the two-of-two
quorum, and the account balance. Any copy that describes a daily cap as an on-chain guarantee is
wrong, and Tidal's documents do not say it. The co-signer additionally enforces the shape of what
it will sign: it rejects any transaction type other than Payment before it even looks up an
approval, and it binds every field of the final transaction — destination, tag, amount, asset and
issuer, fee, the single sequence or ticket, ledger window, an empty signing public key, and the
absence of path or cross-currency fields — to the exact approved intent. One approval yields at most
one signature; a changed fee, ledger window, ticket, or any field is a different request and fails
closed.
Revocation and the emergency exit
Revocation is terminal and immediate: the policy authority stops approving, so the co-signer stops signing, and the agent is left holding a key that can no longer meet quorum. It cannot pay from the budget account anymore. Revocation does not — and cannot — reverse a payment already validated on-ledger; settled XRP and RLUSD are final, and Tidal never uses clawback language for XRP.
The user's exit is independent of Tidal entirely. Because the master key is theirs and stays enabled, they can remove the signer list and sweep the balance back to their main wallet even with every Tidal service offline, submitting directly to a public XRPL node. This is why funding is blocked until the user has completed and verified an offline recovery backup: genuine self-custody means Tidal cannot recover a lost master key, so the model requires the user to prove they can recover it themselves before any money is at stake. The browser or app vault is a convenience copy, never treated as the only copy.
Residual risks worth disclosing
Two honest caveats. First, RLUSD is an issued asset, and issuer powers are outside this model — the issuer retains on-ledger clawback and freeze controls over RLUSD, which is a third-party residual risk to disclose and accept separately, not something Tidal's custody design removes. Second, this model is the accepted architecture direction and is being implemented and hardened; where a specific guarantee depends on a component not yet deployed, that is flagged as draft rather than presented as finished.
Draft — to be finalized. The user-held-master, two-of-two
{agent, co-signer}topology with the policy authority as a pure risk service is the ratified direction, proven end to end on Testnet against a live risk service. Production hardening — the co-signer key in cloud KMS, the full recovery ceremony, and staging deployment — is in progress. Treat the guarantees as the committed design and the deployment status as evolving.
Read next
- Concepts and the integration guide for how these guarantees show up in the objects and the payment lifecycle.
- For users, the plain-language version is in how it works.