Skip to main content

Transactions

Money movement follows build → sign → submit → confirm. Build endpoints return an unsigned transaction plus an intentId; your wallet infrastructure signs and submits; TBook's indexer tracks the result through to settlement (see the intent lifecycle).

All build endpoints: secret or publishable key (contract style with accountId) · support Idempotency-Key · resolve accountId to the account's wallet on the vault's chain. An intentId is recorded only for authenticated calls — anonymous legacy-style calls get a transaction but no intent tracking and no idempotency dedupe.

Common build-time errors:

CodeWhen
invalid_request (400)Account has no wallet on the vault's chain
permission_denied (403)Account is frozen, or the required compliance.kyc/kyb attestation is missing before a first deposit
not_whitelisted (403)XAUa: wallet not yet whitelisted by the issuer (on-chain preflight at build time)
vault_paused (409)Vault paused — deposit/redeem rejected; claim and cancel-deposit stay open
insufficient_balance (400)Wallet lacks the USDC/shares/gas needed to build the transaction, or nothing is claimable

(cycle_closed is defined in the error contract but is not currently emitted — a cycle-window conflict surfaces as an on-chain abort at execution; the transaction never lands, so the intent simply never settles. Use the on-chain result and GET /v1/accounts/:id/positions as the source of truth.)

Common response shape (all four build endpoints):

FieldTypeNotes
intentIdstring?present on authenticated calls — track via lifecycle; appears in webhooks
vaultId, type, chain, railstringecho of what was built
transaction.formatenumsui:tx-bytes-base64
transaction.datastringthe unsigned transaction — sign and submit as-is
summaryobject?human-checkable amounts — see each endpoint for its fields. The price-derived fields (sharePrice, estimatedShares, quotedAt) are rcUSDP-only: they come from the rcUSDP NAV source, so an xaua-sui build omits them rather than pricing gold off a stablecoin's NAV — read their absence as "not quotable here", never as zero. They are also best-effort for rcUSDP (a briefly unavailable price source drops them and the build still succeeds). quotedAt timestamps the shared price snapshot used — all instances quote from the same snapshot, so concurrent builds never disagree on price
messagestringhuman-readable description of the built transaction
expiresAttimestampHow long we expect you to hold the unsigned bytes: ≈10 min. After expiry, rebuild with a new Idempotency-Key — the stale intent expires harmlessly

Built Sui transactions also carry an on-chain expiry. The bytes set ValidDuring.maxEpoch to the epoch after the one they were built in (~24–48h of validity — well beyond the expiresAt above, but finite). Two consequences if you hold funds against a submission:

  • Past that epoch the transaction can no longer execute, so a digest the chain has never seen is permanently absent. That is what makes it safe to release or refund a held position instead of waiting indefinitely — absence alone is never evidence.
  • Do not sit on built bytes for days expecting them to land. Rebuild.

POST /v1/vaults/:vaultId/tx/deposit

Body: {accountId, amount}amount is a decimal string in the asset's currency, ≥ the vault's minDeposit.

On xaua-sui, a deposit is a gold subscription priced at the settlement cycle's NAV.

{
"intentId": "txi_01h…",
"vaultId": "rcusdp-sui", "type": "deposit", "chain": "sui", "rail": "direct",
"transaction": { "format": "sui:tx-bytes-base64", "data": "AAAC…" },
"summary": { "amount": "100", "sharePrice": "1.0275", "estimatedShares": "97.3236", "quotedAt": "2026-06-15T08:07:58Z" },
"message": "Deposit 100.000000 USDC into rcusdp-sui",
"expiresAt": "2026-06-15T08:10:00Z"
}

Extra errors: below_min_deposit (400).

POST /v1/vaults/:vaultId/tx/redeem

Body: {accountId, shares, mode}shares decimal string; mode is "queued" (default) or "instant".

  • Queued — enters the settlement cycle; on settlement the proceeds are paid directly to the account's wallet (transaction.redeem.settled with amount) on rcusdp-sui. Vaults that allocate a claimable balance instead signal it via claimable in the same event — see the event catalog.
  • Instant — only where GET /v1/vaults/:vaultId reports settlement.instantRedeem.available: true with source: "chain" (that value is read from the contract per request). Pays USDC immediately, less the fee. Never quote one on source: "catalog-unverified", and keep a queued fallback: the operator switch can flip between your read and the user's signature.

The summary echoes {shares, mode}.

Extra errors: invalid_request if mode:"instant" is not available on this vault; insufficient_balance if the wallet holds fewer shares than requested.

POST /v1/vaults/:vaultId/tx/claim

Body: {accountId}. Builds a claim of the account's entire claimable balance, paid to the account's own wallet — an on-chain guarantee. Claim works even when a vault is paused.

Only relevant where a settlement allocated a claimable balance — rcusdp-sui currently pays redemption proceeds directly on settlement, so there is normally nothing to claim there.

Extra errors: insufficient_balance (nothing claimable).

POST /v1/vaults/:vaultId/tx/cancel-deposit

Body: {accountId, recordId}recordId comes from positions.pending.deposits[] — each pending record is cancelled individually.

Fees: the vault's cancelFeeBps is withheld from the returned principal — 200 bps (2%) on rcusdp-sui (verified on-chain). The response summary discloses {recordId?, cancelable, cancelFeeBps} plus a note: the on-chain returned amount is authoritative — reconcile against it, not the requested amount.

Extra errors: invalid_request (400) if the vault is not cancelable (xaua-sui declares cancelable: false) or if recordId is missing on the Sui rail.


POST /v1/transactions/:intentId/confirm

Report the chain result after you submit. Requires any valid API key.

Body: {digest}.

Response: {intentId, status} — the intent flips to submitted.

Optional but recommended: webhook correlation becomes exact. If you skip it, the indexer matches the transaction heuristically within about a minute.

Errors: not_found; conflict if the digest is already bound to a different intent.

GET /v1/transactions/:intentId

Requires any valid API key. Returns the full intent record:

{
"intentId": "txi_01h…", "status": "settled", "type": "deposit",
"vaultId": "rcusdp-sui", "accountId": "acct_7f3a…",
"chain": "sui", "walletAddress": "0xabc…",
"amount": "100", "shares": null, "mode": null,
"settlementCycle": 6, "digest": "8vJk…", "expiresAt": "…",
"createdAt": "…", "submittedAt": "…", "confirmedAt": "…", "settledAt": "…",
"failureReason": null
}

Webhooks

transaction.deposit.confirmedtransaction.deposit.settled (or .cancelled), transaction.redeem.confirmedtransaction.redeem.settled, transaction.claim.confirmed. Full catalog: Webhook events.