For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 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.

Handling the 409:

1

The one-shot intent came back 409

POST /orders/intent with onboardIfNeeded: true returned 409 USER_PUBKEY_CONFLICT — the wallet already belongs to a Titan account (yours, the Titan app's, or another partner's).

2

Onboard with SIWS

Collect one SIWS signature and call POST /partner/onboard as above. This proves the user owns the wallet and links your X-Titan-User id to the existing account. Idempotent and safe to retry.

3

Re-issue the intent

Call POST /orders/intent again without onboardIfNeeded (the user is now onboarded), then POST /orders/confirm as normal.

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.

Last updated