> ## Documentation Index
> Fetch the complete documentation index at: https://docs.glider.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Two-Stage Enrollment

> Creating a portfolio for an end-user: signable-message round trip, idempotency, and the fields that must be echoed verbatim.

Enrolling a user into a strategy is a two-stage flow. Stage 1 returns a
message for the user to sign with their wallet; stage 2 submits the signature
and creates the portfolio. The two stages are linked by three round-trip
fields that must be echoed back **byte-for-byte unchanged**.

* Scope: `enroll:write` on both stages.
* Idempotency anchor: `flowId` returned by stage 1. Valid for 24 hours.
* Stage 2 provisions one smart account per requested chain with
  deterministic CREATE2 addresses. The whole operation is atomic.

## Sequence

```
Integrator          Glider API                 End-user Wallet
    |                    |                           |
    |--1. POST /v2/enroll/signature---------------->|
    |                    |                           |
    |<--200 { message, flowId, accountIndex, agentAccountId }
    |                    |                           |
    |--2. signMessage(message.raw)----------------->|
    |                    |                           |
    |<--signature--------|---------------------------|
    |                    |                           |
    |--3. POST /v2/enroll (echo all round-trip fields + signature)
    |                    |                           |
    |<--201 { portfolioId, strategyId, smartAccounts }|
```

## Stage 1 — Request the signable message

`POST /v2/enroll/signature`

Validates that the strategy belongs to your tenant, peeks the user's next
available account index, and returns the session-key message the user must
sign along with the agent wallet that will operate the smart account
post-enrollment.

```bash theme={null}
curl -X POST https://api.glider.fi/v2/enroll/signature \
  -H 'x-api-key: gldr_sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "ownerAccountId": "eip155:0:0xabcdef0000000000000000000000000000000001",
    "strategyId": "01JWZEE2MF30KVRMRX53N88VA4",
    "chainIds": [1, 8453]
  }'
```

Response:

```json theme={null}
{
  "success": true,
  "data": {
    "message": {
      "kind": "ecdsa",
      "raw": "0xdeadbeef...deadbeef"
    },
    "agentAccountId": "eip155:0:0x1111111111111111111111111111111111111111",
    "accountIndex": "7",
    "flowId": "flow_01JWZEE2MF30KVRMRX53N88VA4"
  }
}
```

### Peek, not reserve

`accountIndex` is **peeked**, not reserved. Repeated stage-1 calls for the
same user return the same candidate index, and the index only advances when
stage 2 commits. Retries, abandoned flows, and tests do not burn indices.

### Unsupported chain ids

Unknown chains return `400` with the allowed list in `details`, so you can
surface a helpful error without a second round trip:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "API_400",
    "message": "Request validation failed",
    "details": [
      "chainIds: Unsupported chainId: [999]. Supported: [1, 8453, 42161]"
    ]
  }
}
```

## Stage 2 — Have the user sign, then submit

The user signs `message.raw` in their wallet (ECDSA `personal_sign`). The
signed bytes are the only cryptographic input. Then `POST /v2/enroll` with
every round-trip field from stage 1.

### Round-trip contract

These fields **must be sent back to stage 2 byte-for-byte identical** to
what stage 1 returned. Any mismatch fails signature verification.

| Field            | Stage 1 returns | Stage 2 requires                  |
| ---------------- | --------------- | --------------------------------- |
| `flowId`         | Yes             | Yes — also the idempotency anchor |
| `accountIndex`   | Yes (string)    | Yes — as a string, not a number   |
| `agentAccountId` | Yes             | Yes                               |
| `ownerAccountId` | (input)         | Same CAIP-10 as stage 1           |
| `strategyId`     | (input)         | Same as stage 1                   |
| `chainIds`       | (input)         | Same set, same order              |

```bash theme={null}
curl -X POST https://api.glider.fi/v2/enroll \
  -H 'x-api-key: gldr_sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "ownerAccountId": "eip155:0:0xabcdef0000000000000000000000000000000001",
    "strategyId": "01JWZEE2MF30KVRMRX53N88VA4",
    "chainIds": [1, 8453],
    "accountIndex": "7",
    "agentAccountId": "eip155:0:0x1111111111111111111111111111111111111111",
    "signature": "0x9412d70d...39e01b",
    "flowId": "flow_01JWZEE2MF30KVRMRX53N88VA4",
    "portfolioName": "Alice Balanced"
  }'
```

Response:

```json theme={null}
{
  "success": true,
  "data": {
    "portfolioId": "pf_01JWZG3KH9P4N5QXJVNK7M3WTV",
    "strategyId": "01JWZEE2MF30KVRMRX53N88VA4",
    "smartAccounts": [
      { "accountId": "eip155:1:0x2222222222222222222222222222222222222222" },
      { "accountId": "eip155:8453:0xe3a2d1f49aee887e42655b56371d4d76bbf58058" }
    ]
  }
}
```

## Error Handling

| Code      | HTTP | Cause                                                                                  | Retry safe?                          |
| --------- | ---- | -------------------------------------------------------------------------------------- | ------------------------------------ |
| `API_400` | 400  | Unknown strategy, expired `flowId` (past 24h), invalid signature, or malformed CAIP-10 | No — re-run stage 1                  |
| `API_007` | 409  | A previous stage-2 call with this `flowId` is still executing                          | Yes — back off and retry             |
| `API_008` | 409  | Same `flowId`, different body — replay conflict                                        | No — do not retry with modified body |
| `API_202` | 409  | User is already enrolled in this strategy                                              | No — terminal                        |

See [Idempotency](/guides/idempotency) for the full 409 model.

## Common Pitfalls

* **Modifying `accountIndex` between stages.** `"7"` and `7` are different
  values. The schema requires the decimal string form. Do not parse it to
  an integer on your side unless you also re-serialize it before stage 2.
* **Generating a fresh `flowId` on stage-2 retry.** This starts a new flow
  and invalidates the previous one. Always retry with the original `flowId`
  unless you have received a terminal error.
* **Sending `ownerAccountId` in chain-bound form.** The enroll surface only
  accepts chain-agnostic `eip155:0:<addr>` owners. Use
  [CAIP identifiers](/guides/caip-identifiers) to pick the right form.
* **Calling stage 2 more than 24 hours after stage 1.** The `flowId`
  reservation has expired. Re-run stage 1.
