Onboarding (SIWS)
Bind a user's external wallet to a Titan-managed manager with one SIWS signature.
Every user makes exactly one signature before their first order, whatever its type. POST /partner/onboard takes that Sign-In-with-Solana signature, provisions the user's Titan-managed manager, and records their external wallet as the funding and withdrawal address. After this, you act on the user's behalf with the X-Titan-User header alone — no further per-request signatures.
The call is idempotent and resumable. Retrying with the same sub replays the same userId and walletAddress, so it's safe to call on every login if you'd rather not track who's already onboarded.
What it provisions
A single call resolves or creates the user's Titan identity (namespaced to your tenant), creates their manager with the signing policy baked in, and binds their external wallet to that identity. The policy pins the manager to order-execution swaps — DCA cycles and trigger fills — plus withdrawals only to the user's own external wallet, so neither you nor Titan can move funds anywhere else.
Build the canonical message
The user signs these exact bytes with their external wallet. Address: must equal userPubkey; User: must equal the sub you send in the body.
Titan DCA wants you to link this Solana wallet.
Address: <userPubkey — base58>
User: <your stable user id>
Issued At: <ISO-8601, within ±10 min of now>
Nonce: <opaque random string>function buildSiwsMessage(address: string, sub: string): string {
return (
`Titan DCA wants you to link this Solana wallet.\n\n` +
`Address: ${address}\n` +
`User: ${sub}\n` +
`Issued At: ${new Date().toISOString()}\n` +
`Nonce: ${crypto.randomUUID()}\n` // trailing newline is required
);
}The signed bytes must match the canonical form exactly: LF line endings, a trailing \n after Nonce:, and ≤ 1024 bytes. Issued At must be within ±10 minutes of server time. A mismatch returns 400 SIWS_INVALID.
The user's wallet signs the message bytes in your frontend; you forward the base58 signature to your backend:
Submit it
This call uses X-Titan-Key only — there's no X-Titan-User yet, because this is the call that creates the mapping.
Store userId. It's the opaque Titan id you pass to the partner reporting filters (?userId=…) — it is not your sub. From here on, identify the user on every call with X-Titan-User: <sub>.
Replay and freshness
Titan enforces the ±10-minute Issued At window plus the signature and ownership checks. The Nonce is opaque and not persisted, so it isn't checked for single use — a message can be re-submitted within its freshness window. That's safe because onboard is idempotent: a replay just returns the same userId / walletAddress. Generate a fresh Issued At and nonce per attempt anyway.
Single-transaction onboarding
The flow above asks the user for two signatures before their first order: the SIWS message here, then the deposit transaction. For a brand-new wallet you can collapse that to one. Pass onboardIfNeeded: true on POST /orders/intent, and if the X-Titan-User id was never onboarded, Titan provisions the manager inline and returns the deposit transaction as usual. That deposit is signed by userPubkey, so the signature doubles as the ownership proof — a wrong or unowned address can never fund the order, and until it's signed the manager is an empty wallet whose policy only permits outflows back to userPubkey.
The flag is an explicit opt-in: it must be exactly true, and it only provisions new users. For an already-onboarded user it's ignored (the intent behaves as normal, including 403 VALIDATION_ERROR if userPubkey doesn't match the attested wallet). It never links an additional wallet to an existing user — that still needs the SIWS flow.
Build the two-step fallback before you ship the one-shot path. A wallet that's brand-new to you can still be known to Titan — the user may have used it on the Titan app or through another partner, and wallets are recognized across the whole platform, not just your tenant. When that happens, onboardIfNeeded: true returns 409 USER_PUBKEY_CONFLICT instead of silently attaching you to that account. You must detect this 409 and fall back to the two-step SIWS flow, or those users can't place their first order — and it can happen the very first time you see a user.
Handling the 409:
A brand-new wallet is one signature; a wallet already known to Titan is the usual two. New users — the common case — never hit the 409. Still use POST /partner/onboard directly when you want to bind the wallet ahead of any deposit, or you need the userId for reporting before the first order.
Errors
HTTP
error.code
When
400
BAD_REQUEST
Missing sub / userPubkey / siws.message / siws.signature, or invalid JSON.
400
SIWS_INVALID
Signature, canonical-form, freshness, or ownership check failed — bad signature, Address ≠ userPubkey, User ≠ sub, or Issued At outside ±10 min.
400
PARTNER_NOT_CONFIGURED
Your tenant isn't enabled for partner onboarding.
401
INVALID_API_KEY / KEY_REVOKED / ENV_MISMATCH
Standard X-Titan-Key failures.
409
USER_PUBKEY_CONFLICT
Your sub and the attested wallet resolve to two different existing Titan identities.
409
WALLET_NEEDS_USER_CONSENT
The wallet exists with no manager setup and Titan can't attach one server-side (rare edge).
500 / 502
PROVISIONING_FAILED
Provisioning error (502 upstream, 500 unexpected). Safe to retry — the call is idempotent.
A user-scoped call against a not-fully-provisioned manager returns 409 ONBOARDING_INCOMPLETE. Re-call POST /partner/onboard (idempotent), then retry the original request.
Related pages
Quickstart — onboarding in the context of the full flow
Authentication — how
X-Titan-Userresolves a user after onboardingCreating Orders — the first thing you do once a user is onboarded
Last updated

