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

# Participant Sets

A ParticipantSet lets several independent members authorize one Swig role with
an M-of-N threshold. The set belongs to one Swig config, and the role controls
what the set may do through the same typed actions used by any other role.

Members can use Ed25519, secp256r1/passkeys, or secp256k1. Each member signs a
challenge bound to the prepared operation, while every challenge in the plan
commits to the same shared ParticipantSet nonce.

ParticipantSet support is available in TypeScript `0.10.0` and Python `0.9.0`.

## Create the set and add its role

This 2-of-2 example prepares a SOL-transfer role using two Ed25519 members. First
prepare and submit the ParticipantSet creation transaction. After that
transaction lands, use the general
[Add Roles](/developer-sdk/add-roles-and-permissions) flow to
attach the set to a role and choose its permissions.

The address variables below are base58 strings. `adminPublicKey` /
`admin_public_key` identifies the existing role allowed to add the new role.
`signAndSubmit` / `sign_and_submit` represents your application-owned signing,
RPC submission, and confirmation helper.

`wallets.use` accepts the bare Swig config address used below. Pass a full
wallet reference instead when your application already has both the config and
wallet addresses.

<CodeGroup dropdown>
  ```typescript TypeScript theme={null}
  import { SystemProgram } from '@solana/web3.js';

  const createdSet = await swig.participantSets.create({
    swigConfigAddress,
    feePayer,
    threshold: 2,
    members: [
      { ed25519: { publicKey: participantAPublicKey } },
      { ed25519: { publicKey: participantBPublicKey } },
    ],
  });

  await signAndSubmit(createdSet.transaction);

  const wallet = swig.wallets.use(swigConfigAddress, {
    requesterAuthority: {
      ed25519: { publicKey: adminPublicKey },
    },
  });
  const addRole = await wallet.roles.add({
    feePayer,
    authority: {
      participantSet: { address: createdSet.participantSetAddress },
    },
    actions: [
      {
        type: 'program',
        programId: SystemProgram.programId.toBase58(),
      },
      { type: 'solLimit', amount: 1_000_000n },
    ],
  });

  await signAndSubmit(addRole);
  ```

  ```python Python theme={null}
  from swig_developer_sdk import ProgramAction, SolLimitAction
  from solders.system_program import ID as SYSTEM_PROGRAM_ID

  created_set = await swig.participant_sets.create(
      swig_config_address=swig_config_address,
      fee_payer=fee_payer,
      threshold=2,
      members=(
          {"ed25519": {"publicKey": participant_a_public_key}},
          {"ed25519": {"publicKey": participant_b_public_key}},
      ),
  )

  await sign_and_submit(created_set.transaction)

  wallet = swig.wallets.use(
      swig_config_address,
      requester_authority={
          "ed25519": {"publicKey": admin_public_key}
      },
  )
  add_role = await wallet.roles.add(
      fee_payer=fee_payer,
      authority={
          "participantSet": {"address": created_set.participant_set_address}
      },
      actions=(
          ProgramAction(program_id=str(SYSTEM_PROGRAM_ID)),
          SolLimitAction(amount=1_000_000),
      ),
  )

  await sign_and_submit(add_role)
  ```
</CodeGroup>

Both SDK calls return prepared transactions. Wait for the ParticipantSet
creation transaction to confirm before preparing the add-role transaction.
Creating the set does not add a Swig role automatically.

<Note>
  Keep both actions on a scoped SOL-transfer role. The `program` action permits
  the wallet to invoke Solana's System Program, while `solLimit` caps the
  lamports that may leave the wallet. SignV2 checks these permissions
  independently, so `solLimit` alone does not authorize the program invocation.
  The Developer SDK calls this action `program`; the Protocol SDK builder calls
  the same permission `programLimit`.
</Note>

`solLimit` is the role's remaining one-time SOL budget. Each successful transfer
decrements it, so size the initial allowance for the operations the role should
perform.

The add-role requester must currently be an Ed25519 or secp256r1 authority. A
ParticipantSet can be the new role authority, but cannot request the add-role
operation itself.

## Prepare an operation

After both setup transactions land, select the ParticipantSet as the requester.
Supported preparation routes return a normal prepared transaction with a
`participantSetApprovalPlan` / `participant_set_approval_plan`.

<CodeGroup dropdown>
  ```typescript TypeScript theme={null}
  const participantWallet = swig.wallets.use(swigConfigAddress, {
    requesterAuthority: {
      participantSet: { address: createdSet.participantSetAddress },
    },
  });

  const prepared = await participantWallet.transfer.sol({
    feePayer,
    destination,
    amount: 1_000n,
  });

  const plan = prepared.participantSetApprovalPlan;
  if (!plan) throw new Error('ParticipantSet approval plan is missing');
  ```

  ```python Python theme={null}
  participant_wallet = swig.wallets.use(
      swig_config_address,
      requester_authority={
          "participantSet": {"address": created_set.participant_set_address}
      },
  )

  prepared = await participant_wallet.transfer.sol(
      fee_payer=fee_payer,
      destination=destination,
      amount=1_000,
  )

  plan = prepared.participant_set_approval_plan
  if plan is None:
      raise RuntimeError("ParticipantSet approval plan is missing")
  ```
</CodeGroup>

ParticipantSet preparation currently supports SOL transfers, token transfers,
transfer batches, and custom instructions without address lookup tables. Swaps
and custom transactions with address lookup tables require a direct
non-ParticipantSet requester. Recovery preparation is not currently supported;
see [Guardian recovery](/developer-sdk/use-cases/guardian-recovery) for the
scoped permission model and a possible extension requiring multiple approvals.

Serialize ParticipantSet operations per set. Concurrent plans use the same next
nonce; after one transaction lands, prepare and approve the others again.

## Collect member approvals

Send each selected member only its own approval request. The detached signer
helpers verify that the signer type and public key match that request before
asking the application-owned signer to approve it.

| Member | Helper behavior |
| :- | :- |
| Ed25519 | Signs the decoded 32-byte challenge |
| secp256r1 | Uses the decoded challenge in a WebAuthn assertion and preserves its exact assertion data |
| secp256k1 | Passes the lowercase 64-character ASCII hex challenge to your `personal_sign` callback; the callback must apply EIP-191 |

<CodeGroup dropdown>
  ```typescript TypeScript theme={null}
  import {
    createParticipantEd25519Signer,
    signParticipantSetApproval,
  } from '@swig-wallet/developer-sdk/signers';

  const memberSigners = new Map([
    [
      participantAPublicKey,
      createParticipantEd25519Signer({
        publicKey: participantAPublicKey,
        signMessage: participantASignMessage,
      }),
    ],
    [
      participantBPublicKey,
      createParticipantEd25519Signer({
        publicKey: participantBPublicKey,
        signMessage: participantBSignMessage,
      }),
    ],
  ]);

  const approvals = await Promise.all(
    plan.members.map(async (request) => {
      if (!('ed25519' in request.authority)) {
        throw new Error('Expected an Ed25519 ParticipantSet member');
      }
      const signer = memberSigners.get(request.authority.ed25519.publicKey);
      if (!signer) throw new Error('Approval plan returned an unknown member');
      return signParticipantSetApproval(request, signer);
    }),
  );
  ```

  ```python Python theme={null}
  from swig_developer_sdk.signers import (
      create_participant_ed25519_signer,
      sign_participant_set_approval,
  )

  member_signers = {
      participant_a_public_key: create_participant_ed25519_signer(
          public_key=participant_a_public_key,
          sign_message=participant_a_sign_message,
      ),
      participant_b_public_key: create_participant_ed25519_signer(
          public_key=participant_b_public_key,
          sign_message=participant_b_sign_message,
      ),
  }

  approvals = []
  for request in plan.members:
      authority = request.authority.get("ed25519")
      if authority is None:
          raise RuntimeError("Expected an Ed25519 ParticipantSet member")
      public_key = authority["publicKey"]
      signer = member_signers.get(public_key)
      if signer is None:
          raise RuntimeError("Approval plan returned an unknown member")
      approvals.append(await sign_participant_set_approval(request, signer))
  ```
</CodeGroup>

Production applications normally distribute each request to its owning member
and collect the resulting approvals out of band. The in-process 2-of-2 example
keeps both Ed25519 callbacks together only to show the complete data flow.

Your application remains responsible for passkey credential selection and its
RP ID/origin policy. Do not ask a member to sign the raw serialized transaction
or another member's challenge.

## Compile and submit

Return the detached approvals with the original prepared transaction. The API
validates the plan and simulates the final transaction before returning it.

<CodeGroup dropdown>
  ```typescript TypeScript theme={null}
  const compiled = await swig.transactions.compileParticipantSetApprovals({
    preparedTransaction: prepared,
    approvals,
  });

  console.log(compiled.authorizationExpirationSlot);
  await signAndSubmit(compiled.transaction);
  ```

  ```python Python theme={null}
  compiled = await swig.transactions.compile_participant_set_approvals(
      prepared_transaction=prepared,
      approvals=approvals,
  )

  print(compiled.authorization_expiration_slot)
  await sign_and_submit(compiled.transaction)
  ```
</CodeGroup>

Compilation does not sponsor, broadcast, or confirm the transaction. Add every
remaining Solana transaction signature, then submit directly or use
[Sponsor & Submit](/developer-sdk/sponsor-and-submit) before the authorization
expiration slot. If the slot has passed, prepare a fresh operation and collect
fresh approvals.

Compilation also rejects insufficient or duplicate approvals and a plan whose
shared nonce no longer matches the on-chain ParticipantSet. Prepare a fresh
operation after another transaction consumes the nonce.

## Approval and submission boundaries

* Prepare a fresh plan and collect fresh approvals for each distinct
  transaction.
* Keep every approval with its original plan and shared nonce.
* Keep the Swig API key in the server client; detached signer helpers accept no
  API key and make no hosted API calls.
* Add transaction-level signatures such as the fee payer or another required
  Ed25519 signer after compilation.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.