> For the complete documentation index, see [llms.txt](https://spree-finance.gitbook.io/spreefinance/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://spree-finance.gitbook.io/spreefinance/spree-studio/whitelist-management.md).

# 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
