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:
| Code | When |
|---|---|
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):
| Field | Type | Notes |
|---|---|---|
intentId | string? | present on authenticated calls — track via lifecycle; appears in webhooks |
vaultId, type, chain, rail | string | echo of what was built |
transaction.format | enum | sui:tx-bytes-base64 |
transaction.data | string | the unsigned transaction — sign and submit as-is |
summary | object? | 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 |
message | string | human-readable description of the built transaction |
expiresAt | timestamp | How 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.settledwithamount) onrcusdp-sui. Vaults that allocate a claimable balance instead signal it viaclaimablein the same event — see the event catalog. - Instant — only where
GET /v1/vaults/:vaultIdreportssettlement.instantRedeem.available: truewithsource: "chain"(that value is read from the contract per request). Pays USDC immediately, less the fee. Never quote one onsource: "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.confirmed → transaction.deposit.settled (or .cancelled), transaction.redeem.confirmed → transaction.redeem.settled, transaction.claim.confirmed. Full catalog: Webhook events.