Developer docs

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.

KeyHeld bySignsCannot
MasterThe user, in their Tidal wallet (encrypted at rest)Structural transactions: funding, TrustSet, SignerListSet, emergency sweepNever signs an agent's day-to-day payment
AgentYou, the developer, in your KMS — private half never leavesThe first signature on a paymentCannot meet quorum alone; cannot change its own limits
Co-signerTidal's co-signer service, in HSM/KMSThe second signature, only after policy approvalCannot 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 &amp; 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