Skip to main content

Self-Custody

Self-Custody is the platform's real, operating execution model today — the only ExecutionCustodyMode that can actually be selected for any Environment, Sandbox or Production. Under Self-Custody, the platform never holds a private key on your behalf — the wallet is generated and controlled entirely on your side, through one of the official SDKs.

ExecutionCustodyMode also declares ManagedCustody, ExternalCustody, and SmartContractEscrow at the domain level — preserved architecturally (so the platform's own history and internal tooling, like TreasuryReconciliation, keep working against that vocabulary), but none of them can be activated: setting a Environment's mode to anything other than Self-Custody is rejected outright. Treat those three as reserved, not as configuration options you can choose today.

How it works

  1. Local wallet generation. The SDK generates a BIP39 mnemonic and derives a BIP32 extended key locally (BIP44 path m/44'/195'/0', TRON coin_type = 195). The private key is created and used entirely inside your process — it is never transmitted, logged, or persisted by the SDK on its own (INV-SC-01).
  2. Public registration. Only the resulting extended public key (xpub) is sent to the API — POST /v1/applications/{applicationId}/wallets. The platform can derive addresses from it but can never sign with it.
  3. Deposit address allocation. The platform derives a real TRON deposit address from the registered xpub (POST .../wallets/deposit-addresses) — the same SDK can independently re-derive and verify that address locally, as a defense-in-depth check against a compromised backend.
  4. Multi-leg signing. A SigningRequest describes one or more ExecutionLegs (for example, a Seller leg and a Platform Fee leg from the same Settlement). For each leg, the backend computes a deterministic canonical hash of the exact transaction contents; the SDK signs that hash locally with the private key and submits the signature back (POST .../legs/{executionLegId}/submit). The backend independently verifies every signature against the same canonical hash before accepting it — a tampered amount or destination produces a different hash and is rejected outright.
  5. All-signatures gate. No leg is broadcast to the network until every leg on the SigningRequest has a verified signature. The first signature alone never triggers a broadcast — only the last one does, for all legs at once.

Multi-source funding

A Settlement can be funded by more than one confirmed deposit address — supported: it produces one SigningRequest per physical funding source (SettlementResponse.signingRequestIds, plural), sign and confirm each one. A Withdrawal always sources from a single SigningRequest — multi-source funding is not supported for Withdrawal today (Withdrawal.signingRequestId stays singular-only); don't build an integration that assumes parity here.

What never happens

  • The private key is never sent to the API, in any request, at any step.
  • No leg is broadcast before all legs are signed and verified.
  • A signature is never accepted without independently recomputing and matching the canonical hash server-side.

Errors

A signature submitted for a leg whose recomputed hash doesn't match is rejected with SIGNED_TRANSACTION_MISMATCH — the leg is never broadcast. A fine-grained mismatch reason (amount, participant, network, or asset) is a documented future extension; today the check is a single opaque hash comparison, which already covers the core security invariant: a tampered transaction never passes verification, whatever the specific field that was altered.

Available today

Self-Custody wallet generation, registration, deposit address allocation, and the full multi-leg signing protocol — canonical hash, signature verification, the all-signatures gate, and broadcast — are implemented and validated end to end in Sandbox, through all four official SDKs (Java, Node.js/TypeScript, Python, Go) — each ships a complete, runnable example (13-self-custody-signing). See the marketplace journey guide for this protocol connected end to end with a real payment — signup, Payment Intent, deposit, and a locally signed payout — not just the signing step in isolation.

SigningRequest creation is already wired automatically into both Settlement and Withdrawals — confirmed live: executeSettlement() and withdrawals.request() build the SigningRequest/ExecutionLegs themselves, from the beneficiaries and amounts already on the Settlement/Withdrawal; an integrator never constructs a SigningRequest by hand for those flows (the direct POST .../signing-requests route below still exists for other, lower-level use). Broadcast-confirmation tracking is also automatic, event-driven end to end — once every leg is signed and the all-signatures gate broadcasts, the platform itself observes confirmation (simulated in Sandbox) and moves the Settlement/Withdrawal to its terminal state; the integrator never polls a raw blockchain node for this.

Two things must be registered once per Organization/AssetNetwork before the first real Self-Custody execution can complete: an ExecutionDestination for every beneficiary that gets paid (including the platform's own Fee, under a Settlement), and a NetworkCostPayerAccount to cover the real network resources a broadcast consumes (see Transaction, Settlement, Split, and Refund for both, and Network Execution for ExecutionSource — a third, related registration that's easy to confuse with the other two). Real blockchain execution — an actual transaction landing on a real network — is Production-only and not available yet; Sandbox's broadcast and confirmation are simulated end to end.

Routes

  • Register a wallet: POST /v1/applications/{applicationId}/wallets
  • Allocate a deposit address: POST /v1/applications/{applicationId}/wallets/deposit-addresses
  • Get a wallet: GET /v1/wallets/{walletId} (and /public-material)
  • Create a signing request: POST /v1/environments/{environmentId}/signing-requests
  • Get a signing request: GET /v1/signing-requests/{signingRequestId}
  • Submit a signed leg: POST /v1/signing-requests/{signingRequestId}/legs/{executionLegId}/submit