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.
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).
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.
Each feature solves a problem the previous one creates:
- Omnibus pools custody: one omnibus wallet holds funds on-chain, while off-chain virtual accounts track each tenant's position.
- 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.
- 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. - 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:
Work through this list completely before starting. Most failed automation setups trace back to a skipped prerequisite rather than a failed step.
| 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 |
| 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 |
| 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 |
| Gas Station service | Deploy Gas Station for the target networks. | Gas Station reference |
| Supported ledger | Activate a Solana, XRPL, or EVM network that carries the non-native asset you custody. | Supported ledgers |
| 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 |
| A vault | The omnibus wallet is a regular custody account backed by a vault. | Vaults |
| 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 |
| 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 |
| 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 |
| 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 |
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.
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 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.
- 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
SystemSignedpolicy 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.
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.
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.
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.
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.
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.
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:
{
"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. Matchcontext.submitter.custodyRoles, and where your deployment issues stable service subjects, tighten further withcontext.submitter.subjectso a role assignment alone is not enough. The role name (omnibus-serviceabove) must match a value in the service caller'scustody_rolestoken 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.
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.
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.
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.
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.
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.
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}"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.
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.
- Create the omnibus wallet (user-signed). Initiate the creation flow with
alias,vaultId, andkeyStrategy, 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 toPOST /v1/intentsthrough the normal user-signed flow. - 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 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:
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.
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:
- 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).
- Enable rippling on the issuer: set the
asfDefaultRippleflag on the issuing account (anAccountSettransaction). This must happen before the holders create their trustlines, or the sweep from deposit wallet to omnibus wallet has no ripple path. - Create trustlines to the issuer from the omnibus wallet and from each deposit wallet (
TrustSettransactions).
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.
Prove every link in the chain with one test deposit on a test network.
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.
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_CreateAccountintent asynchronously. The201 Createdresponse is only an acknowledgment: a brand-new wallet's first response is alwaysstatus: CREATINGwith emptyaddresses, returned before the Gateway, Notary, and policy processing run.Poll the same endpoint (
GET .../tenants/{tenantId}/deposit-wallet) until the wallet reachesstatus: ACTIVEwith 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 noFAILEDstatus (statusis onlyCREATING,ACTIVE, orINACTIVE), so a wallet that stays inCREATINGis the failure signal, almost always the Step 4 policy: a missingintentOrigin: "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-sponsorsreturnsisSponsored: truewith your Step 5 sponsor account, proof that domain-level sponsorship covers lazily created deposit wallets.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.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.
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_ReleaseQuarantinedTransfersintent and the sweep follows.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.
Confirm the credit to the tenant: the virtual account's available balance increases only after the Accounting Service records the
OMNIBUSDEPOSITentry. See Balances and reconciliation.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
vaultandgas stationaccount 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.
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.
Automation shifts your effort from signing to supervising. Monitor, at minimum:
- Sponsor account balances against
alertLimitthresholds, 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
vaultandgas stationtypes) 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:
| 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. |
| 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. |
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. |
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 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. |
- System-signed intent configuration: deployment shapes, chart fields, control points, security considerations
- Setup and governance: the omnibus creation flow and required policy in detail
- Deposits, Withdrawals, Internal transfers: the operational flows this tutorial automates
- Manage a gas station with the API: the full sponsorship API
- Submit a system-signed intent with the API: the request structure for building your own service automation
- Policy examples: more system-signed policy patterns
- Data export: reconciliation-grade Position and Movement Reports covering the omnibus wallet, deposit wallets, and sponsor account