Skip to main content
POST
Stage 2 of the two-stage enrollment flow. Takes the flowId, accountIndex, and agentAccountId returned by POST /v2/enroll/signature, plus the user’s signature, and provisions the portfolio: one smart account per requested chain, plus the rebalance schedule derived from your tenant’s execution config. The whole operation succeeds atomically — a failure anywhere rolls back cleanly.
  • Auth: x-api-key header (required)
  • Scope: enroll:write
The endpoint is idempotent on flowId (scoped to your API key) with a 24-hour TTL:
  • Replaying the same flowId with the same body returns the cached response.
  • Replaying with a different body returns 409 IDEMPOTENCY_KEY_CONFLICT.
  • Replaying while the original is still in-flight returns 409 IDEMPOTENCY_IN_PROGRESS.
  • Replaying after a typed error (e.g., signature mismatch) returns the same typed error so retries don’t get stuck.
For EVM, the signature is computed by signing message.raw from the stage-1 response with the user’s wallet, using viem’s signMessage({ message: { raw } }) or the equivalent wallet primitive. For EVM smart-contract wallets, pass accountType: "ERC1271" in both stages and have the user sign the stage-1 message.typedData EIP-712 payload. The server verifies the signature with ERC-1271 on the requested chain and configures the Kernel vault with the SmartAccount sudo validator. ERC-1271 enrollment currently supports exactly one EVM chainId per signature. Wallet addresses are exchanged as CAIP-10 account identifiers. EVM ownerAccountId / agentAccountId use the chain-agnostic form eip155:0:<address>, and each element of the response smartAccounts array uses the chain-bound form eip155:<chainId>:<address>.

Solana (SVM) enrollment

For a Solana-rooted user, omit signature and instead send signedSolanaTransaction — the base64 solanaTransaction from stage 1 after the user signs it with their wallet. The server re-derives the expected instructions, verifies the user signed, co-signs with the pooled agent, and submits it (the paymaster pays). For an EVM-rooted user (Model A subaccount, client-driven single submit), send userSecp256k1PublicKey (the user’s secp256k1 key — its derived address must equal ownerAccountId’s), signature (the user’s slot-bound secp256k1 authorization over the Swig add-authority payload), currentSlot (the slot it was bound to), and optionally ownerSignatureScheme (eip191 default / raw). The server replays the signature to add the agent + create the sub-account in one paymaster-sponsored transaction. Capture the slot and submit promptly — Swig enforces a slot window. The response smartAccounts[] entry’s accountId is the sub-account the user funds; it is also surfaced as depositAccountId, with swigRoleId identifying the owning Swig role. The parent Swig PDA is internal. A second enroll for the same (tenant, user) reuses the same Swig and returns a new sub-account at a higher role. portfolioName is an optional display name for the portfolio. It is trimmed server-side, returned by portfolio read endpoints, and mutable via PATCH /v2/portfolios/{portfolioId}. Names may be up to 64 characters and may include Unicode letters, digits, spaces, and - _ . ' : ( ) & /. Common error responses:
  • 400 for an invalid request body, expired flow, mismatched signature, or invalid strategy ownership
  • 401 when x-api-key header is missing or the key is invalid
  • 403 when the API key lacks the enroll:write scope
  • 409 IDEMPOTENCY_IN_PROGRESS when an earlier request with the same flowId is still running
  • 409 IDEMPOTENCY_KEY_CONFLICT when the same flowId is reused with a different request body
  • 409 PORTFOLIO_ALREADY_EXISTS when the user is already enrolled at the given accountIndex
  • 500 on unexpected server errors