# Whitelist Management

SP and branded SP are transfer-restricted by default. Mint, transfer, and redeem flows are gated to addresses on the corresponding allowlist. This makes loyalty supply controllable — only your treasury operators can mint, only authorized addresses can hold the token, only verified counterparties can redeem to collateral.

This guide covers the three allowlists, their contract surface, and a worked example for delegating issuance to a partner without giving them total control.

## The Three Allowlists

Each allowlist is gated by its own role. The role you need depends on which allowlist you're modifying.

| Allowlist    | Required role                  | What it controls                                 |
| ------------ | ------------------------------ | ------------------------------------------------ |
| **Mint**     | `MANAGER_ROLE` on Factory      | Address can call mint paths through the Factory  |
| **Transfer** | `DEFAULT_ADMIN_ROLE` on Points | Address can hold and move your branded SP token  |
| **Redeem**   | `MANAGER_ROLE` on Factory      | Address can redeem branded SP back to collateral |

`MANAGER_ROLE` is auto-granted to your deployer wallet at deploy completion (see [Prerequisites](/spreefinance/spree-studio/prerequisites.md)). `DEFAULT_ADMIN_ROLE` is held by your deployer wallet by default; migrate to a multisig for production.

> **Note: pSP issuance is not whitelist-gated.** The three allowlists above gate operations on **branded SP** (Factory + Points contracts). For **pSP** (PSPVault) there is no separate "mint whitelist"; minting authority comes from one of: `MANAGER_ROLE`, `ISSUING_ADMIN_ROLE` (broad — mint into any active campaign), per-campaign admin status, or a per-campaign delegated minter configuration set via `setMinter(campaignId, minter, limit, active)`. See the [Partner A → Partner B walkthrough](#worked-example-partner-a--partner-b-partial-delegation) below for scoped delegation using the last path.

## Same Operation, Three Ways

Every allowlist add/remove can be performed via Studio UI, SDK, or HTTP API. All three paths produce the same on-chain transaction.

| Allowlist | Studio UI                                                                                                    | SDK                                      | HTTP API                                        |
| --------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------- | ----------------------------------------------- |
| Mint      | `/sp/integrations` → **Mint Whitelist** card → Add to Whitelist → opens "Add Mint Authority" modal           | `factory.addToMintWhitelist(account)`    | `POST /studio/sdk/v1/tx/whitelist/mint/add`     |
| Transfer  | `/sp/integrations` → **Transfer Whitelist** card → Add to Whitelist → opens "Add Transfer Destination" modal | `points.addToTransferWhitelist(account)` | `POST /studio/sdk/v1/tx/whitelist/transfer/add` |
| Redeem    | `/sp/integrations` → **Redeem Whitelist** card → Add to Whitelist → opens "Add Redeem Authority" modal       | `factory.addToRedeemWhitelist(account)`  | `POST /studio/sdk/v1/tx/whitelist/redeem/add`   |

Read paths:

* `GET /studio/sdk/v1/factory/whitelist/:address` returns mint and redeem status for an address.
* `points.isTransferWhitelisted(account)` returns transfer-allowlist status.

The HTTP API endpoints return an **unsigned transaction** — the caller signs and broadcasts. This is automation-friendly: any backend, in any language, can drive whitelist operations on testnet or mainnet.

## Removing an Address

Symmetric to adding:

| Allowlist | SDK                                           | HTTP API                                           |
| --------- | --------------------------------------------- | -------------------------------------------------- |
| Mint      | `factory.removeFromMintWhitelist(account)`    | `POST /studio/sdk/v1/tx/whitelist/mint/remove`     |
| Transfer  | `points.removeFromTransferWhitelist(account)` | `POST /studio/sdk/v1/tx/whitelist/transfer/remove` |
| Redeem    | `factory.removeFromRedeemWhitelist(account)`  | `POST /studio/sdk/v1/tx/whitelist/redeem/remove`   |

## Worked Example: Partner A → Partner B Partial Delegation

A real-world scenario from partner setups:

* **Partner A** is the brand owner. Holds `DEFAULT_ADMIN_ROLE` and the auto-granted operational roles. Deployed the program. Created **Campaign X**, which makes them automatically a campaign admin on Campaign X.
* **Partner B** is an external partner. Will run reward distribution for **Campaign X only**.

Partner A wants Partner B to mint pSP into Campaign X up to a fixed budget — and nothing else. Partner B should not be able to mint into other campaigns, create new campaigns, change fees, pause anything, or grant further roles.

The PSPVault supports this directly through **per-campaign delegated minters**. Partner A configures Partner B as a minter on Campaign X with an explicit `limit`; Partner B's authority is hard-capped to that one campaign and that one limit. **No on-chain role grant is required**, and Partner B does not need to be on any Factory whitelist (those gate branded SP, not pSP).

### 1. Configure Partner B as a Delegated Minter on Campaign X

Partner A calls `setMinter(campaignX, partnerB, limit, active=true)`. This authorises Partner B to call `pendingSPVault.mint(campaignX, ...)` up to `limit` total pSP. Once `issued >= limit` the mint reverts; Partner A can call `setMinter` again with a new (higher) limit to extend.

The call requires the caller to be either a global `MANAGER_ROLE` holder or an admin on Campaign X. Partner A satisfies the second condition automatically as the campaign creator.

| Path      | Where                                                                                                  |
| --------- | ------------------------------------------------------------------------------------------------------ |
| Studio UI | `/campaigns/:id` → **Authority** tab → Configure minter → Partner B's address + limit                  |
| SDK       | `pendingSPVault.setMinter(campaignX, partnerB, limit, true)`                                           |
| HTTP API  | `POST /studio/sdk/v1/tx/psp/configure-minter` with `{ campaignId, minter, limit, active: true, from }` |

To pause Partner B's authority without revoking the configuration (e.g., to throttle issuance during an incident), call `setMinter(campaignX, partnerB, limit, false)`. To reactivate, set `active=true` again.

### 2. Verify the Partition

Partner A reads back Partner B's minter config to confirm the partition. Partner B should hold no on-chain roles and should appear in the minter table for **only** Campaign X.

| Path            | Where                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Studio UI       | `/campaigns/:id` → Authority tab — minter table shows Partner B authorised on Campaign X with the configured limit and `active=true` |
| Studio UI (alt) | `/admin` — role roster confirms Partner B holds no on-chain roles                                                                    |
| SDK             | `pendingSPVault.getMinterConfig(campaignX, partnerB)` returns `{ limit, issued, active }`                                            |
| HTTP API        | `GET /studio/sdk/v1/psp/minter-config/:campaignId/:minter`                                                                           |

A negative test confirms scope:

* `pendingSPVault.createCampaign(...)` from Partner B reverts (no `MANAGER_ROLE` or `CAMPAIGN_ADMIN_ROLE`).
* `pendingSPVault.mint(campaignY, ...)` from Partner B reverts (`MinterNotAuthorized` — no minter config on Campaign Y).
* `pendingSPVault.mint(campaignX, ...)` from Partner B succeeds, up to the configured `limit`.

### 3. Revoke When the Engagement Ends

Deactivate the minter config:

| Path      | Where                                                                                                      |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| Studio UI | `/campaigns/:id` → Authority tab → Deactivate minter                                                       |
| SDK       | `pendingSPVault.setMinter(campaignX, partnerB, 0, false)`                                                  |
| HTTP API  | `POST /studio/sdk/v1/tx/psp/configure-minter` with `{ campaignId, minter, limit: 0, active: false, from }` |

Partner B's row in the minter table now shows `active=false` and Partner B can no longer call `mint`.

### Broader Alternative: Grant `ISSUING_ADMIN_ROLE`

If Partner B needs to mint pSP across **any** active campaign on this PSPVault (not just Campaign X) — for example, an internal distribution service that drives issuance for every campaign you ever launch — grant `ISSUING_ADMIN_ROLE` instead. This role lets the holder call `mint(...)` on any active campaign without per-campaign minter configuration, but the holder is still budget-limited per campaign.

| Path      | Where                                                                                           |
| --------- | ----------------------------------------------------------------------------------------------- |
| Studio UI | `/campaigns/:id` → **Authority** tab → Grant role → ISSUING\_ADMIN                              |
| SDK       | `pendingSPVault.grantRole(ISSUING_ADMIN_ROLE, partnerB)`                                        |
| HTTP API  | `POST /studio/sdk/v1/tx/psp/grant-role` with `{ roleType: 'ISSUING_ADMIN', account: partnerB }` |

This is a broader grant than the delegated-minter path above. Pick whichever matches the trust level you intend.

### When Partner B Also Needs to Mint Branded SP

If the partnership scope expands so Partner B needs to mint *branded SP* directly — for example, to mint into a custodial wallet — Partner A must additionally add Partner B to the Factory's mint whitelist. Branded SP minting and pSP issuance are gated independently; you can grant one, both, or neither.

| Path      | Where                                                                                                        |
| --------- | ------------------------------------------------------------------------------------------------------------ |
| Studio UI | `/sp/integrations` → **Mint Whitelist** card → Add to Whitelist → "Add Mint Authority" → Partner B's address |
| SDK       | `factory.addToMintWhitelist(partnerB)`                                                                       |
| HTTP API  | `POST /studio/sdk/v1/tx/whitelist/mint/add` with `{ account: partnerB }`                                     |

## Best Practices

* **Add partners before they need to interact.** Whitelist additions don't propagate instantly; budget time before launch.
* **Audit allowlists regularly.** `/sp/integrations` shows the current state at a glance.
* **Revoke promptly when an engagement ends.** Treat allowlist removal and role revocation as part of every off-boarding checklist.
* **Use distinct addresses per role.** Treasury, ops, external partners — keep them on separate wallets so revoking one doesn't disrupt another.

## Related

* [Authority & Roles](/spreefinance/spree-studio/authority-and-roles.md) — full reference for role grants and revokes
* [Campaigns & Pending SP](/spreefinance/spree-studio/campaigns-and-pending-sp.md) — what `mint(...)` actually does once delegation is set up
* [Roles & Access](/spreefinance/spree-studio/roles-and-access.md) — the role taxonomy


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://spree-finance.gitbook.io/spreefinance/spree-studio/whitelist-management.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
