# 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](/products/custody/deployment/reference/system-signed-intents#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](/products/custody/accounts-and-assets/omnibus/overview#how-omnibus-works) 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](/products/custody/accounts-and-assets/gas-station/overview#omnibus-usage) 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:

```mermaid
sequenceDiagram
    actor Tenant
    participant Omnibus as Omnibus Service
    participant Custody as Custody platform<br/>(Gateway, Notary, policies)
    participant GasStation as Gas Station
    participant Ledger as Blockchain
    participant Accounting as Accounting Service

    Tenant->>Omnibus: Request a deposit address
    Omnibus->>Custody: Create deposit wallet<br/>(system-signed v0_CreateAccount)
    Note over Custody: Gateway signs internally,<br/>Notary verifies the registered key,<br/>a SystemSigned policy admits the intent
    Custody-->>Omnibus: Deposit wallet created
    Omnibus-->>Tenant: Deposit address
    Tenant->>Ledger: Send tokens
    Ledger-->>Custody: Deposit detected and confirmed
    Custody-->>Omnibus: Deposit released from quarantine
    Omnibus->>Custody: Sweep to omnibus wallet<br/>(system-signed v0_CreateTransactionOrder)
    GasStation->>Ledger: Fund the sweep fee just in time<br/>(system-signed, from the sponsor account)
    Custody->>Ledger: Broadcast the sweep
    Ledger-->>Omnibus: Sweep confirmed
    Omnibus->>Accounting: Credit the tenant's virtual account
```

## 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

| Prerequisite | Details | Reference |
|  --- | --- | --- |
| Current on-premise release package | System-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](/products/custody/deployment/reference/system-signed-intents) |
| Gateway signing component | A 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](/products/custody/deployment/reference/system-signed-intents#deployment-patterns) |
| Omnibus service and Accounting Service | Deploy both and confirm they are reachable; the Accounting Service is the source of truth for virtual account balances. | [Omnibus deployment reference](/products/custody/deployment/reference/omnibus) |
| Gas Station service | Deploy Gas Station for the target networks. | [Gas Station reference](/products/custody/deployment/reference/gas-station) |
| Supported ledger | Activate a Solana, XRPL, or EVM network that carries the non-native asset you custody. | [Supported ledgers](/products/custody/reference/supported-ledgers) |


### Governance and identity prerequisites

| Prerequisite | Details | Reference |
|  --- | --- | --- |
| A domain for the omnibus structure | Each omnibus structure belongs to one domain. Decide the domain before writing policies, because you scope the system-signed policy to it. | [Domains](/products/custody/governance/domains) |
| A vault | The omnibus wallet is a regular custody account backed by a vault. | [Vaults](/products/custody/identity-and-access/vault-management) |
| Service-caller identity for Omnibus | Register 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](/products/custody/identity-and-access/authentication/authenticate-api-requests#obtain-a-token-for-a-service-caller) |
| Service-caller identity for Gas Station | Gas 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](/products/custody/deployment/reference/gas-station#migrate-from-a-bot-user) |
| Users able to submit and approve governance intents | Steps 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](/products/custody/governance/intents/manage-intents-and-approvals) |
| Funded sponsor account | A 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](/products/custody/accounts-and-assets/gas-station/overview) |


### Knowledge prerequisites

You should be comfortable with the [intent proposal, approval, and signing lifecycle](/products/custody/governance/intents), [policy structure and evaluation](/products/custody/governance/policies), and [authenticating API requests](/products/custody/identity-and-access/authentication/authenticate-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 discover | How | Used in |
|  --- | --- | --- |
| Whether system signing is already active | `GET /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 identities | List 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 pairing | List 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 funding | `GET /v1/domains/{domainId}/sponsors`, then each sponsor's configuration and native-token balances. | Step 5 |
| Sweep thresholds for your asset | `GET /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 ranks | List 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 vault | List 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](/products/custody/deployment/reference/system-signed-intents#automatic-key-provisioning).

Confirm the signer is wired and a key exists:

```sh
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`:

```json
{
  "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](/products/custody/deployment/reference/system-signed-intents#chart-level-fields) and [example](/products/custody/deployment/reference/system-signed-intents#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](/products/custody/governance/genesis/payload-reference#system-signatures-public-key). On a running deployment, submit a **user-signed** `v0_RegisterTrustedPublicKey` intent:

```json
"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:

```json
"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-8-operate-monitor-and-know-your-kill-switches).

## 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 operation | Intent type | Why Omnibus needs it |
|  --- | --- | --- |
| Create deposit wallets | `v0_CreateAccount` | Omnibus creates deposit wallets lazily when a tenant first requests a deposit address. |
| Sweep deposits | `v0_CreateTransactionOrder` | Confirmed, quarantine-released deposits move on-chain from deposit wallets into the omnibus wallet. |
| Propagate ledgers | `v0_AddAccountLedgers` | When you add a ledger to the omnibus wallet, deposit wallets inherit it. |


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

```json
{
  "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`:

```json
{
  "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](/products/custody/accounts-and-assets/gas-station/overview#system-signed-intents-and-policies) and the [policy example](/products/custody/governance/policies/examples#system-signed-gas-station-proposal).

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](/products/custody/accounts-and-assets/gas-station/manage-gas-station-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:

```sh
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:

```sh
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](/products/custody/compliance/transaction-screening/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:

```sh
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](/products/custody/accounts-and-assets/gas-station/overview#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](/products/custody/accounts-and-assets/omnibus/setup-and-governance) and [Manage omnibus with the API](/products/custody/accounts-and-assets/omnibus/manage-omnibus-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:

`CREATING` → `PENDING_SPONSORSHIP` → `ACTIVE`

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:

```sh
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.

## 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](/products/custody/accounts-and-assets/omnibus/manage-omnibus-api).
2. **Request a deposit wallet** for it:

```sh
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](/products/custody/identity-and-access/authentication/authenticate-api-requests#service-caller-identity-submitter-author-and-requester).
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](/products/custody/accounts-and-assets/omnibus/manage-omnibus-api#manage-sweep-thresholds).
5. **Watch the deposit lifecycle**: confirmation, quarantine release, gas funding, then the system-signed sweep. [Deposit processing](/products/custody/accounts-and-assets/omnibus/deposits#deposit-processing) describes the stage-by-stage balance effects.
Omnibus does not sweep quarantined funds. Unless an [automatic quarantine release policy](/products/custody/compliance/transaction-screening/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**:

```sh
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](/products/custody/accounts-and-assets/omnibus/balances-and-reconciliation).
8. **Export the audit evidence** (optional): generate a [Movement Report](/products/custody/data-export/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.

```sh
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](/products/custody/data-export/position-report) snapshot and reconcile sweep activity with [Movement Reports](/products/custody/data-export/movement-report). 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](/products/custody/accounts-and-assets/omnibus/manage-omnibus-api#manage-sweep-thresholds); no redeployment needed.


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

| Switch | Effect | How |
|  --- | --- | --- |
| Disable the runtime property | Stops **all** system-signed processing platform-wide. | User-signed `v0_SetSystemProperty` with `enabled: false` (Step 3 in reverse). |
| Lock or tighten the policy | Stops 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 account | Stops the service authenticating at all. | At your identity provider. |


## Troubleshooting

| Symptom | Likely cause | Check |
|  --- | --- | --- |
| Deposit-wallet request fails; system-signed proposals rejected | A 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 sign | The 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 fail | Condition 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 execute | The 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 swept | The 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](/products/custody/accounts-and-assets/omnibus/deposits). |
| `availableAmount` is `0` and dry-runs fail with `Not enough available funds`, despite correct on-chain balances | Transaction 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](/products/custody/compliance/transaction-screening/automatic-quarantine-release-policy). |
| Calls that use a newly created ticker fail with `Ticker ... is not validated` | `v0_CreateTicker` creates the ticker but does not activate it. | Submit `v0_ValidateTickers` for the created ticker. |
| XRPL sweep fails on a missing trustline or path | Ledger propagation does not create IOU trustlines, and the omnibus wallet may still be inactive on-chain. | The [XRPL trustline setup](#xrpl-only-set-up-trustlines-by-hand) 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 deposits | The sponsor account is not in the omnibus ignored-account list. | Omnibus ignored-account configuration (Step 6). |
| Sponsorship stalls with healthy balances | The 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](/products/custody/accounts-and-assets/gas-station/overview#system-signed-intents-and-policies). |


## Related topics

- [System-signed intent configuration](/products/custody/deployment/reference/system-signed-intents): deployment shapes, chart fields, control points, security considerations
- [Setup and governance](/products/custody/accounts-and-assets/omnibus/setup-and-governance): the omnibus creation flow and required policy in detail
- [Deposits](/products/custody/accounts-and-assets/omnibus/deposits), [Withdrawals](/products/custody/accounts-and-assets/omnibus/withdrawals), [Internal transfers](/products/custody/accounts-and-assets/omnibus/internal-transfers): the operational flows this tutorial automates
- [Manage a gas station with the API](/products/custody/accounts-and-assets/gas-station/manage-gas-station-api): the full sponsorship API
- [Submit a system-signed intent with the API](/products/custody/governance/intents/manage-intents-and-approvals#submit-a-system-signed-intent-with-the-api): the request structure for building your own service automation
- [Policy examples](/products/custody/governance/policies/examples#system-signed-gas-station-proposal): more system-signed policy patterns
- [Data export](/products/custody/data-export/overview): reconciliation-grade Position and Movement Reports covering the omnibus wallet, deposit wallets, and sponsor account