Skip to content

Tutorial: Automate omnibus operations with system-signed intents and Gas Station

Executive summary

This tutorial builds a pooled-custody pipeline that runs itself: Omnibus pools the funds, system-signed intents do the signing, and Gas Station pays the network fees.

Each feature solves the problem the previous one creates, which is why you deploy them together:

  • Omnibus lets one on-chain wallet serve many customers. Each customer gets a personal deposit address, confirmed deposits are swept into the shared wallet, and an off-chain ledger tracks exactly who owns what.
  • That convenience creates work: every deposit address and every sweep is an intent that someone must sign, and at exchange scale that means thousands of signatures a day. System-signed intents let the platform sign this repetitive work itself, under a narrow standing policy that you approve once.
  • Sweeps still cost gas, and fresh deposit wallets hold only tokens. Gas Station sends each wallet just enough of the native token, just in time, so sweeps never stall waiting for funds.

Take away any one piece and the pipeline stops: without Omnibus there is nothing to pool, without system signing a human signs every sweep, and without Gas Station sweeps cannot pay their fees. At the end, a tenant deposit flows from an external wallet into the omnibus wallet with zero manual signing, and every automated step stays policy-governed and auditable. Expect the full walkthrough to take a working session for two users.

Who this tutorial is for

This tutorial is for teams that run high-volume, multi-tenant custody on a self-hosted Ripple Custody deployment: exchanges, brokers, asset managers, and platform businesses that give each customer a deposit address while settling against pooled funds.

You need two users, because the work deliberately crosses a trust boundary:

  • One user manages the deployment: they verify the Gateway signing component, deploy Gas Station, and fund the sponsor account (Steps 1 and 5).
  • One user with admin permissions handles governance: they register the signing key, enable runtime processing, create and approve the automation policies, and review the omnibus structure (Steps 2–4 and 6).

When to use this pattern

Use this pattern when you custody non-native assets (tokens such as stablecoins) on a supported Solana, XRPL, or EVM network, your deposit volume makes per-deposit human action infeasible, and your governance process can approve a narrowly scoped standing policy in place of per-intent human approval. You need an on-premise deployment: system-signed intent configuration applies to on-premise deployments only.

Do not use this pattern when:

  • Your volume is low enough that the standard user-signed intent flow already fits.
  • Regulation or internal policy requires a human approval on every fund movement. An approval workflow on sweep intents blocks the automation this tutorial builds.
  • You want to automate governance-changing operations such as policy creation or user management. Those must stay on the user-signed path. See Security considerations.

Why these three features work together

Each feature solves a problem the previous one creates:

  1. Omnibus pools custody: one omnibus wallet holds funds on-chain, while off-chain virtual accounts track each tenant's position.
  2. Pooling hides who deposited what, so Omnibus creates a deposit wallet per tenant and sweeps each confirmed deposit into the omnibus wallet. Every wallet creation and every sweep is an intent; at exchange scale, someone has to sign thousands of them per day.
  3. System-signed intents remove the signing bottleneck: the platform signs these repetitive intents internally, and a policy with intentOrigin: "SystemSigned" acts as the standing approval that admits them. Every automated intent stays in the audit trail.
  4. Sweeps still need the ledger's native token for fees, and deposit wallets hold only tokens. Gas Station funds the fee just in time from a sponsor account, and Omnibus ignores that account as a deposit source so funding never counts as a customer deposit.

The diagram shows how the pieces interact for one tenant deposit. No human touches it, and policy governs every step:

Accounting ServiceBlockchainGas StationCustody platform(Gateway, Notary, policies)Omnibus ServiceAccounting ServiceBlockchainGas StationCustody platform(Gateway, Notary, policies)Omnibus ServiceGateway signs internally,Notary verifies the registered key,a SystemSigned policy admits the intentTenantRequest a deposit addressCreate deposit wallet(system-signed v0_CreateAccount)Deposit wallet createdDeposit addressSend tokensDeposit detected and confirmedDeposit released from quarantineSweep to omnibus wallet(system-signed v0_CreateTransactionOrder)Fund the sweep fee just in time(system-signed, from the sponsor account)Broadcast the sweepSweep confirmedCredit the tenant's virtual accountTenant
Accounting ServiceBlockchainGas StationCustody platform(Gateway, Notary, policies)Omnibus ServiceAccounting ServiceBlockchainGas StationCustody platform(Gateway, Notary, policies)Omnibus ServiceGateway signs internally,Notary verifies the registered key,a SystemSigned policy admits the intentTenantRequest a deposit addressCreate deposit wallet(system-signed v0_CreateAccount)Deposit wallet createdDeposit addressSend tokensDeposit detected and confirmedDeposit released from quarantineSweep to omnibus wallet(system-signed v0_CreateTransactionOrder)Fund the sweep fee just in time(system-signed, from the sponsor account)Broadcast the sweepSweep confirmedCredit the tenant's virtual accountTenant

Prerequisites

Work through this list completely before starting. Most failed automation setups trace back to a skipped prerequisite rather than a failed step.

Deployment prerequisites

PrerequisiteDetailsReference
Current on-premise release packageSystem-signed intent configuration ships enabled by default at deployment level in current release packages. Confirm your chart includes the systemSignedIntents section.System-signed intent configuration
Gateway signing componentA dedicated KMS Connect instance for Gateway (sidecar or standalone), commonly platform: kms_soft, with its master wrapping key stored as a secret. Do not reuse the Notary KMS Connect instance.Deployment patterns
Omnibus service and Accounting ServiceDeploy both and confirm they are reachable; the Accounting Service is the source of truth for virtual account balances.Omnibus deployment reference
Gas Station serviceDeploy Gas Station for the target networks.Gas Station reference
Supported ledgerActivate a Solana, XRPL, or EVM network that carries the non-native asset you custody.Supported ledgers

Governance and identity prerequisites

PrerequisiteDetailsReference
A domain for the omnibus structureEach omnibus structure belongs to one domain. Decide the domain before writing policies, because you scope the system-signed policy to it.Domains
A vaultThe omnibus wallet is a regular custody account backed by a vault.Vaults
Service-caller identity for OmnibusRegister the Omnibus service in your identity provider (Keycloak) so it obtains a JWT whose custody_roles claim carries a role your policies can match (for example omnibus-service). Govern this role assignment with the same change control as custody policies.Obtain a token for a service caller
Service-caller identity for Gas StationGas Station submits funding transactions as system-signed intents; it holds no user signing key. Register the Gas Station service caller in Keycloak with its own custody_roles value (for example gas-station-service). If you are upgrading a deployment that used a bot user for funding, complete the migration before relying on sponsorship.Migrate from a bot user
Users able to submit and approve governance intentsSteps 2–4 submit v0_RegisterTrustedPublicKey, v0_SetSystemProperty, and v0_CreatePolicy, all user-signed intents that flow through your normal approval workflows.Manage intents and approvals
Funded sponsor accountA custody account on the target ledger holding enough native tokens to fund sweeps, with alertLimit thresholds planned. A sponsor account funds only accounts on the same network.Gas Station

Knowledge prerequisites

You should be comfortable with the intent proposal, approval, and signing lifecycle, policy structure and evaluation, and authenticating API requests. This tutorial builds on all three.

Preflight: discover your instance's values

Every example value in this tutorial (role names, ranks, sample IDs) is a placeholder. On a running instance, discover the real values with the reads below and substitute them throughout. Each read maps to the step that consumes it.

What to discoverHowUsed in
Whether system signing is already activeGET /v1/system-signing/info returns an active key. Read the runtime property with GET /v1/properties and check that NOTARY_SYSTEM_SIGNED_INTENTS_ENABLED reports enabled: true. Executed system-signed intents are additional evidence: on a read, a service-authored intent identifies its submitter in details.author (a service subject, not a user) and shows state.status: "Executed". If both checks pass, Steps 2 and 3 are already complete.Steps 1–3
Your service callers' real identitiesList intents in a domain with system-signed activity and inspect the author of a system-signed record. Use the exact subject and custody_roles values you find in your Step 4 policy conditions.Step 4
A valid asset pairingList the tickers in your deployment and confirm that one supported ledger (Solana, XRPL, or EVM) carries both the non-native token you custody and its native ticker. Without both halves on a single ledger, deposits can't be swept and sweeps can't pay fees; stop and activate the missing asset before continuing. To activate a token, submit a user-signed v0_CreateTicker intent and then v0_ValidateTickers for the created ticker: creation alone leaves the ticker unusable, and calls that touch it fail with Ticker ... is not validated.Steps 5–7
Existing sponsors and their fundingGET /v1/domains/{domainId}/sponsors, then each sponsor's configuration and native-token balances.Step 5
Sweep thresholds for your assetGET /v1/domains/{domainId}/sweep-thresholds?ledgerId={ledgerId}. Assets without a configured threshold default to 0 and sweep immediately. Note your asset's minSweepAmount: the test deposit in Step 7 must exceed it.Step 7
Existing policies and their ranksList the policies in the target domain and choose a rank that orders correctly against them. The rank: 400 in this tutorial is only an example.Step 4
A vaultList vaults, reuse an existing one, and note its supported key strategies.Step 6
The policy condition must match your real service identity

The most common way this setup fails is silent: the policy condition names a role (such as omnibus-service) that your service's JWT does not actually carry, so proposals never match any policy and fail closed, with no error pointing at the policy. Before you write the Step 4 condition, read the author of a real system-signed intent on your instance and match those exact values. Some deployments identify the service by subject (for example metaco_internal) rather than by a custody role.

Working on a live instance
  • Step 3's runtime property is platform-wide, not per domain. On an instance that already runs system-signed automation it is already enabled. Verify instead of toggling, and treat any change to it as a change-control event.
  • Create the omnibus structure and its policies in a dedicated domain. A SystemSigned policy is a standing, unattended approval; scope: "Self" in a domain of its own bounds the blast radius, away from your established accounts and policies.
  • Step 5's sponsorship call replaces the target account's existing sponsor configuration. Read the current configuration first, and prefer a dedicated sponsor account over reusing one that already serves another structure.

Step 1: Verify the Gateway signing component

Gateway provisions its Ed25519 system signing keypair automatically on first boot: it asks its dedicated KMS Connect instance to generate the keypair, then stores the wrapped private key in the operations.gateway_system_signing_key table. Gateway never holds the unwrapped private key. For the mechanism, see Automatic key provisioning.

Confirm the signer is wired and a key exists:

curl -X GET "${CUSTODY_API_URL}/v1/system-signing/info" \
  -H "Authorization: Bearer ${JWT_TOKEN}"

A healthy response lists at least one key with active: true:

{
  "signingKeys": [
    {
      "publicKey": "MCowBQYDK2VwAyEA...",
      "createdAt": "2026-06-26T12:00:00.000Z",
      "active": true
    }
  ]
}

Record the active publicKey value; you register it in the next step.

If the call fails or returns no keys, check the systemSignedIntents chart values and the Gateway signing configuration under silo.gateway.system-signed-intents.kms-connect. See the chart-level fields and example.

Fail-closed by design

Everything past this point fails closed. A deployment with the signer wired but no registered key, or a registered key but a disabled runtime property, or both in place but no matching policy, rejects system-signed proposals. Work the steps in order and verify each before moving on.

Step 2: Register the Gateway public key in Notary

Notary does not automatically trust the Gateway-minted key. Notary trusts only the key referenced by the governed NOTARY_SYSTEM_SIGNING_KEY system property, and Gateway refuses to sign until its key matches the registered one.

If you are launching a new deployment, you can register the key during Genesis. On a running deployment, submit a user-signed v0_RegisterTrustedPublicKey intent:

"payload": {
  "publicKey": "MCowBQYDK2VwAyEA...",
  "purpose": "SystemSignatures",
  "type": "v0_RegisterTrustedPublicKey"
}

When this intent executes, Notary adds the key to the trusted public key collection and sets NOTARY_SYSTEM_SIGNING_KEY to it.

The legacy v0_AddTrustedPublicKeysForMigration intent is not a registration path for SystemSignatures keys, and this release does not provide an API to rotate or regenerate the Gateway signing key.

Step 3: Enable runtime processing

Registration alone does not activate the flow. Submit a user-signed v0_SetSystemProperty intent to flip the governed runtime switch:

"payload": {
  "value": {
    "type": "NotarySystemSignedIntentsEnabledProperty",
    "value": {
      "enabled": true
    }
  },
  "type": "v0_SetSystemProperty"
}

This property applies to the entire instance, not to your domain. Read its current state with GET /v1/properties: the response lists NOTARY_SYSTEM_SIGNED_INTENTS_ENABLED with its current enabled value. On a shared or long-running instance, verify before you submit. Don't re-toggle a switch that other automation already depends on.

This property is also your kill switch: setting enabled: false through the same governed flow stops all system-signed processing immediately, without redeployment. Note the incident-response value of this; see Step 8.

Step 4: Create the automation policies

This is the governance heart of the tutorial. The policy is the standing approval for automated traffic, so its scope determines your risk exposure. You create two policies.

The Omnibus automation policy

Omnibus needs exactly three intent types. Grant nothing more:

Automated operationIntent typeWhy Omnibus needs it
Create deposit walletsv0_CreateAccountOmnibus creates deposit wallets lazily when a tenant first requests a deposit address.
Sweep depositsv0_CreateTransactionOrderConfirmed, quarantine-released deposits move on-chain from deposit wallets into the omnibus wallet.
Propagate ledgersv0_AddAccountLedgersWhen you add a ledger to the omnibus wallet, deposit wallets inherit it.

Submit a user-signed v0_CreatePolicy intent in the omnibus domain:

{
  "id": "<policy-uuid>",
  "alias": "omnibus-system-signed-automation",
  "rank": 400,
  "scope": "Self",
  "intentTypes": [
    "v0_CreateAccount",
    "v0_CreateTransactionOrder",
    "v0_AddAccountLedgers"
  ],
  "intentOrigin": "SystemSigned",
  "scriptingEngine": "Javascript_v0",
  "condition": {
    "expression": "context.submitter.type === 'Service' && context.submitter.custodyRoles.includes('omnibus-service')",
    "type": "Expression"
  },
  "lock": "Unlocked",
  "description": "Allows the Omnibus service to create deposit wallets, sweep deposits, and propagate ledgers.",
  "customProperties": {},
  "type": "v0_CreatePolicy"
}

Every element of this policy is doing security work:

  • intentOrigin: "SystemSigned": the policy matches only system-signed proposals. A user-signed intent can never satisfy it, and user-signed policies can never admit system-signed traffic. The platform evaluates the two populations separately.
  • intentTypes: the explicit allowlist. If Omnibus ever submits any other intent type, no policy matches and the proposal fails closed.
  • condition: identifies the service caller. Match context.submitter.custodyRoles, and where your deployment issues stable service subjects, tighten further with context.submitter.subject so a role assignment alone is not enough. The role name (omnibus-service above) must match a value in the service caller's custody_roles token claim.
  • scope: "Self": confines the policy to the omnibus domain rather than its descendants.
  • No workflow: omitting the workflow makes execution automatic, which is the point of this tutorial. This must be a deliberate, assessed decision. If you add a workflow, user-signed approvers must satisfy it (the service submitter never counts as an approver) and sweeps will queue on human approval.
Create the policy with v0_CreatePolicy, not domain genesis

Define system-signed policies with standalone v0_CreatePolicy or v0_UpdatePolicy intents. Policies defined inline in a domain-genesis payload do not persist intentOrigin; the result is an ordinary user-signed policy that never matches automated traffic. If you already created the policy through genesis, correct it with v0_UpdatePolicy; read the policy immediately beforehand, because the update requires the current reference.revision, and revisions move on an active instance.

Verify the policy after the intent executes: retrieve it and confirm that intentOrigin is "SystemSigned" and the condition carries your discovered service identity. intentOrigin is exactly the field that goes missing when you create the policy the wrong way, and its absence produces no error, only proposals that never match.

Policy governance

Never include policy-management intent types (such as v0_CreatePolicy or v0_UpdatePolicy) in a system-signed policy. Policy creation changes the governance model itself and must remain under independent, user-signed policy-management approvals.

The Gas Station automation policy

Gas Station funding follows the same system-signed model as Omnibus: the service authenticates as a Keycloak-registered service caller and submits funding transaction orders with no user signing key. It needs a second, even narrower policy that grants only v0_CreateTransactionOrder:

{
  "id": "<policy-uuid>",
  "alias": "gas-station-system-signed",
  "rank": 410,
  "scope": "Self",
  "intentTypes": ["v0_CreateTransactionOrder"],
  "intentOrigin": "SystemSigned",
  "scriptingEngine": "Javascript_v0",
  "condition": {
    "expression": "context.submitter.custodyRoles.includes('gas-station-service')",
    "type": "Expression"
  },
  "lock": "Unlocked",
  "customProperties": {},
  "type": "v0_CreatePolicy"
}

Set scope to match how far sponsorship reaches: Self for a single domain, or SelfAndDescendants when Gas Station sponsors accounts in child domains (includeSubDomains in Step 5). If this policy requires manual approvals, funding queues on human action and sponsorship stalls. For the full pattern, see System-signed intents and policies and the policy example.

Keep the two policies separate, one per service caller. A single shared policy would let either service submit the union of both intent-type sets, widening each service's blast radius for no operational gain.

Step 5: Configure Gas Station sponsorship

With governance in place, wire up fee funding. All Gas Station steps use the sponsorship API.

Before you write any sponsorship, check whether the account you plan to use already has a configuration: creating a sponsorship replaces the account's existing one, which can silently break another structure that depends on it:

curl -X GET "${CUSTODY_API_URL}/v1/domains/{domainId}/account/{sponsorAccountId}/sponsor" \
  -H "Authorization: Bearer ${JWT_TOKEN}"

A 404 means the account has no configuration and is safe to use. Then sponsor the omnibus domain so that sponsorship automatically covers every lazily created deposit wallet; account-level sponsorship would require an update per new deposit wallet, which defeats the automation:

curl -X POST "${CUSTODY_API_URL}/v1/domains/{domainId}/account/{sponsorAccountId}/sponsor" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "domain",
    "includeSubDomains": false,
    "alertLimit": [
      {
        "tickerId": "native-token-ticker-uuid",
        "amount": "5.0"
      }
    ]
  }'

Set alertLimit to a threshold that gives your operations team time to top up the sponsor account before it runs dry: an empty sponsor account silently stalls every sweep on that ledger.

Quarantine applies to your own funding transfers too

On an instance with transaction screening enabled, screening quarantines every incoming transfer, including the native tokens you send to fund the sponsor account, and later the omnibus wallet. The symptom is confusing: the account's on-chain balance is correct, but availableAmount stays 0 and dry-runs fail with Not enough available funds. After funding any account in this tutorial, check its balances, and release the held transfers with a v0_ReleaseQuarantinedTransfers intent (find them under the account's incoming transfers), or configure an automatic quarantine release policy.

Path change in version 1.38

Version 1.38 moves the Gas Station API to the plural /v1/domains/{domainId}/... paths used in this tutorial and deprecates the singular /v1/domain/{domainId}/... forms. If your instance runs an earlier Gas Station build and these calls return 404, use the singular paths.

Verify the configuration:

curl -X GET "${CUSTODY_API_URL}/v1/domains/{domainId}/account/{sponsorAccountId}/sponsor" \
  -H "Authorization: Bearer ${JWT_TOKEN}"
Ignore the sponsor as a deposit source

When Gas Station funds a deposit wallet, that funding transaction arrives on-chain like any other incoming transfer. Omnibus must ignore the Gas Station sponsor account as a deposit source, or it processes funding transactions as customer deposits. Configure the ignored-account settings when you create the omnibus structure in the next step. See Omnibus usage.

Step 6: Create the omnibus structure

Omnibus creation is deliberately a two-phase flow, and the split reflects the trust model: a human decision creates the pooled wallet; automation handles everything repeatable after that.

  1. Create the omnibus wallet (user-signed). Initiate the creation flow with alias, vaultId, and keyStrategy, plus optional ledger, description, custom-property, ignored-account, and Gas Station settings; include the sponsor account from Step 5 in the ignored accounts. Omnibus validates the domain, prepares a custody account creation intent, and returns the omnibus record with an unsigned Custody intent payload. Sign it and submit it to POST /v1/intents through the normal user-signed flow.
  2. Automation setup (Omnibus service). Omnibus creates the host virtual account, initializes Accounting Service state, configures the omnibus wallet to skip quarantine for intra-domain sweeps, and checks Gas Station sponsorship from Step 5.

For the request details, sequence diagram, and operational split of responsibilities, see Setup and governance and Manage omnibus with the API.

Omnibus creation does not validate the Step 4 policy: you create the policy upstream, and no step of the creation flow checks it. A missing or wrong policy surfaces later, as a deposit wallet that never leaves CREATING in Step 7. Verify the policy directly before continuing: re-read it and confirm that it still carries intentOrigin: "SystemSigned" and that its condition matches the identity the service actually submits with (Step 4).

The omnibus status lifecycle

The structure does not become operational in one step. Its status moves through:

CREATINGPENDING_SPONSORSHIPACTIVE

A fourth status, FAILED, marks a structure whose creation did not complete.

If you did not link a sponsor at creation, the structure sits in PENDING_SPONSORSHIP: created, but not operational. It becomes ACTIVE when you link the sponsor account from Step 5:

curl -X PUT "${CUSTODY_API_URL}/v1/domains/{domainId}/omnibus/{omnibusId}" \
  -H "Authorization: Bearer ${JWT_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "sponsoringGasStationId": "<sponsor-account-uuid>"
  }'

The update accepts only sponsoringGasStationId; it does not accept ignoredAccountIds, so set the ignored accounts when you create the structure. After linking, retrieve the omnibus and confirm that status is ACTIVE and that your sponsor account appears in ignoredAccountIds.

XRPL only: set up trustlines by hand

Omnibus propagates ledgers to deposit wallets, but on XRPL, ledger propagation does not create IOU trustlines, and a new omnibus wallet stays inactive on-chain until it holds the XRP base reserve. For an XRPL IOU, complete these steps in order before sending any test deposit:

  1. Activate the omnibus wallet on-chain: send it enough XRP to meet the base reserve (and release the funding from quarantine, as in Step 5).
  2. Enable rippling on the issuer: set the asfDefaultRipple flag on the issuing account (an AccountSet transaction). This must happen before the holders create their trustlines, or the sweep from deposit wallet to omnibus wallet has no ripple path.
  3. Create trustlines to the issuer from the omnibus wallet and from each deposit wallet (TrustSet transactions).

Without these steps, deposits land but sweeps fail on the missing trustline or ripple path. Solana and EVM ledgers do not need equivalent setup.

Complete all trustline and IOU setup before the first deposit, and never run it concurrently with deposit activity. A TrustSet that lands while a native XRP sweep is in flight can leave the wallet below its increased required reserve, and the sweep's Payment transaction then fails on-chain with tecUNFUNDED_PAYMENT while still burning the network fee. Each trustline adds the XRPL owner reserve (currently 0.2 XRP) on top of the base reserve, so confirm that each wallet holds enough XRP for every object it owns. You can prefund deposit wallets from the Gas Station sponsor account. See XRPL IOU readiness.

Step 7: Verify the pipeline end to end

Prove every link in the chain with one test deposit on a test network.

  1. Create a virtual account for a throwaway test tenant. Validate the pipeline with a disposable tenant and a small deposit, never against a real customer's virtual account. See Manage omnibus with the API.

  2. Request a deposit wallet for it:

    curl -X POST "${CUSTODY_API_URL}/v1/domains/{domainId}/omnibus/{omnibusId}/tenants/{tenantId}/deposit-wallet" \
      -H "Authorization: Bearer ${JWT_TOKEN}"

    This is a direct Omnibus API call; it does not return an unsigned intent for you to sign. Behind it, Omnibus records the wallet and submits the system-signed v0_CreateAccount intent asynchronously. The 201 Created response is only an acknowledgment: a brand-new wallet's first response is always status: CREATING with empty addresses, returned before the Gateway, Notary, and policy processing run.

    Poll the same endpoint (GET .../tenants/{tenantId}/deposit-wallet) until the wallet reaches status: ACTIVE with a populated address. That is your first proof that the entire system-signed chain works: Gateway signed, Notary verified the registered key, the runtime property allowed processing, and your policy matched. There is no FAILED status (status is only CREATING, ACTIVE, or INACTIVE), so a wallet that stays in CREATING is the failure signal, almost always the Step 4 policy: a missing intentOrigin: "SystemSigned", or a condition that does not match the actual submitter.

    Before sending funds, confirm that Gas Station coverage reaches the new wallet: GET /v1/domains/{domainId}/sponsors/account/{depositWalletAccountId}/valid-sponsors returns isSponsored: true with your Step 5 sponsor account, proof that domain-level sponsorship covers lazily created deposit wallets.

  3. Inspect the automated intent. Retrieve recent intents in the domain and find the account creation. Service-authored records identify the submitter in author.subject / requester.subject. Confirm it is the Omnibus service caller, not a user. See Service-caller identity.

  4. Send a test deposit of the supported non-native asset to the deposit address, just above the sweep dust threshold from your preflight read: large enough to sweep, small enough to be harmless. To check or change the threshold, use the sweep thresholds API.

  5. Watch the deposit lifecycle: confirmation, quarantine release, gas funding, then the system-signed sweep. Deposit processing describes the stage-by-stage balance effects.

    Omnibus does not sweep quarantined funds. Unless an automatic quarantine release policy covers the transfer, the deposit sits in quarantine and the sweep never fires; release it with a v0_ReleaseQuarantinedTransfers intent and the sweep follows.

  6. Confirm gas sponsorship happened:

    curl -X GET "${CUSTODY_API_URL}/v1/domains/{domainId}/sponsor/events?limit=100" \
      -H "Authorization: Bearer ${JWT_TOKEN}"

    An empty result here is not necessarily a failure: if the deposit wallet already held enough native tokens, the sweep pays its own fee and Gas Station never needs to fund it. To prove just-in-time funding explicitly, repeat the test with a fresh deposit wallet that holds no native tokens; Gas Station must then fund it before the sweep can move the token.

  7. Confirm the credit to the tenant: the virtual account's available balance increases only after the Accounting Service records the OMNIBUS DEPOSIT entry. See Balances and reconciliation.

  8. Export the audit evidence (optional): generate a Movement Report for the omnibus domain covering the test window. One file captures the on-chain story you just verified (the inbound test deposit, the Gas Station funding transfer with senderAccountType: gas station, and the sweep into the omnibus wallet) with request metadata and control totals, so you can attach it to your change record as reconciliation-grade evidence of the test. You need a role with read access to transactions in the omnibus domain.

    curl -X POST "${CUSTODY_API_URL}/v1/exports/movement" \
      -H "Authorization: Bearer ${JWT_TOKEN}" \
      -H "Content-Type: application/json" \
      -d '{
        "domainId": "<omnibus-domain-uuid>",
        "dateRange": {
          "start": "<test-window-start>",
          "end": "<test-window-end>"
        },
        "format": "CSV"
      }'

    Exports cover custody accounts, currently the vault and gas station account types, which include the omnibus wallet, deposit wallets, and the sponsor account. Tenant virtual accounts are not included in exports in this release, so the tenant-side proof remains the Accounting Service credit from the previous check.

If all seven verification checks pass, the pipeline is operational.

Watching the pipeline from your existing event consumers

This release does not deliver Omnibus-level events to external consumers, so your existing webhook and channel endpoints don't see omnibus events directly. To observe the pipeline from your event infrastructure, watch the underlying intent and transaction events: the system-signed intent executions and the sweep transaction status changes.

Step 8: Operate, monitor, and know your kill switches

Automation shifts your effort from signing to supervising. Monitor, at minimum:

  • Sponsor account balances against alertLimit thresholds, funding failures, and retry queues; an empty sponsor stalls sweeps silently.
  • System-signed intent activity per service caller: signature verification failures and unusual request rates are your earliest compromise indicators.
  • Omnibus reconciliation: the on-chain omnibus wallet balance against summed virtual account balances, and the host virtual account (which legitimately goes negative through fee compensation, but should trend predictably). For the on-chain side, schedule a daily Position Report snapshot and reconcile sweep activity with Movement Reports. The export service is REST-only and synchronous, so it fits the same scheduled-job pattern as the automation this tutorial builds. Exports cover custody accounts (the vault and gas station types) in this release; the virtual-account side of the reconciliation still comes from Accounting Service records.
  • Pending deposits stuck below the dust threshold or in quarantine. You can adjust thresholds at runtime, per domain, with the sweep thresholds API; no redeployment needed.

When something is wrong, you have three graduated kill switches, from broadest to narrowest:

SwitchEffectHow
Disable the runtime propertyStops all system-signed processing platform-wide.User-signed v0_SetSystemProperty with enabled: false (Step 3 in reverse).
Lock or tighten the policyStops the specific service's automation while other system-signed traffic continues.User-signed v0_LockPolicy to lock the policy, or v0_UpdatePolicy to narrow its conditions.
Disable the service accountStops the service authenticating at all.At your identity provider.

Troubleshooting

SymptomLikely causeCheck
Deposit-wallet request fails; system-signed proposals rejectedA link in the activation chain is missing: signer, key registration, runtime property, or policy.Work Steps 1–4 in order: GET /v1/system-signing/info returns an active key; you registered the key with purpose: "SystemSignatures"; NOTARY_SYSTEM_SIGNED_INTENTS_ENABLED is enabled: true; a SystemSigned policy matches the domain, intent type, and submitter.
Gateway refuses to signThe active Gateway key does not match the registered NOTARY_SYSTEM_SIGNING_KEY.Compare GET /v1/system-signing/info output with the registered key.
Policy exists but proposals still failCondition mismatch: the service JWT does not carry the expected role or subject.Inspect the rejected request's author.subject and the JWT's custody roles against the policy expression.
Sweeps queue but never executeThe policy includes an approval workflow; the service submitter does not count as an approver.Remove the workflow if automatic execution is the assessed intent, or staff the approval.
Deposits confirmed but never sweptThe deposit is still quarantined; insufficient gas sponsorship; ledger mismatch between deposit wallet and omnibus wallet; or amount below the dust threshold.The deposit wallet's quarantinedAmount (release with v0_ReleaseQuarantinedTransfers); sponsor balance and GET .../sponsor/events; sponsorship coverage with GET .../sponsors/account/{accountId}/valid-sponsors; the asset's threshold with GET /v1/domains/{domainId}/sweep-thresholds; ledger inheritance; failed and pending deposits.
availableAmount is 0 and dry-runs fail with Not enough available funds, despite correct on-chain balancesTransaction screening quarantined the account's incoming transfers, including your own funding transfers.The account's quarantinedAmount; release the held transfers with v0_ReleaseQuarantinedTransfers, or configure an automatic quarantine release policy.
Calls that use a newly created ticker fail with Ticker ... is not validatedv0_CreateTicker creates the ticker but does not activate it.Submit v0_ValidateTickers for the created ticker.
XRPL sweep fails on a missing trustline or pathLedger propagation does not create IOU trustlines, and the omnibus wallet may still be inactive on-chain.The XRPL trustline setup in Step 6: XRP-activate the omnibus wallet, set asfDefaultRipple on the issuer, then TrustSet on the omnibus and deposit wallets.
Funding transactions appear as tenant depositsThe sponsor account is not in the omnibus ignored-account list.Omnibus ignored-account configuration (Step 6).
Sponsorship stalls with healthy balancesThe Gas Station system-signed policy is missing, does not match the service caller's custody_roles, or includes an approval workflow.The gas-station-system-signed policy state and the funding intents' rejection reasons; see System-signed intents and policies.