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
- Local wallet generation. The SDK generates a BIP39 mnemonic and derives a BIP32 extended
key locally (BIP44 path
m/44'/195'/0', TRONcoin_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). - 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. - 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. - Multi-leg signing. A
SigningRequestdescribes one or moreExecutionLegs (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. - All-signatures gate. No leg is broadcast to the network until every leg on the
SigningRequesthas 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