Launch Types
Project Vesting
Last updated August 24, 2026
Genesis project vesting uses a ClaimScheduleBucketV2 to release one token allocation to one recipient according to an onchain cliff and periodic linear schedule.
What You'll Build
This guide creates a one-year project token vesting allocation with a 10% cliff, monthly linear unlocks, and optional authority controls.
Summary
ClaimScheduleBucketV2 is a first-class Genesis outflow bucket for project, team, advisor, or treasury token vesting. It stores the recipient, allocation, claim history, vesting curve, pause state, policy controls, and optional cancellation behavior onchain.
- One bucket vests one token allocation to one current recipient
- A
ClaimSchedulecombines an independent cliff with period-based linear unlocks - Claims are permissionless but always pay the bucket's recipient
- Optional policies support authority pauses, cancellation, recipient cancellation, and recipient transfers
Jump to: Quick Start · Vesting Mechanics · Runtime Controls · Cancellation · Reference
ClaimScheduleBucketV2 and ClaimSchedule
ClaimScheduleBucketV2 is the account that owns a project allocation, while ClaimSchedule is the reusable vesting curve embedded in that account.
| Type | Purpose |
|---|---|
ClaimScheduleBucketV2 | Stores one recipient, allocation, claimed amount, schedule, claim gate, pause state, policies, and end behaviors |
ClaimSchedule | Defines the cliff amount, cliff condition, linear start condition, duration, and unlock period |
ClaimScheduleV2Extensions | Stores runtime policy flags and an optional backend claim signer |
Genesis has no ClaimScheduleBucketV1. Use the V2 account and instruction names for project vesting. A ClaimSchedule is also used by other bucket extensions, so the schedule type alone does not identify a project vesting bucket.
Quick Start
The quick start adds a one-year vesting bucket with a 10% cliff and monthly linear unlocks to an initialized but not yet finalized Genesis V2 account.
Complete Genesis setup first and reserve the vesting allocation from the base token supply. The sum of all bucket allocations must fit within the Genesis account's total supply.
Create a Project Vesting Bucket
addClaimScheduleBucketV2 creates the bucket before the Genesis account is finalized.
1import {
2 addClaimScheduleBucketV2,
3 createClaimSchedule,
4 createTimeAbsoluteCondition,
5 findClaimScheduleBucketV2Pda,
6 genesis,
7} from '@metaplex-foundation/genesis'
8import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
9import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
10
11const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
12
13// umi.use(keypairIdentity(yourKeypair))
14
15const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
16const baseMint = publicKey('YOUR_PROJECT_TOKEN_MINT')
17const recipient = publicKey('VESTING_RECIPIENT')
18const bucketIndex = 0
19
20const [vestingBucket] = findClaimScheduleBucketV2Pda(umi, {
21 genesisAccount,
22 bucketIndex,
23})
24
25const DAY = 86_400n
26const vestingStart = BigInt(Math.floor(Date.now() / 1000)) + 7n * DAY
27const vestingEnd = vestingStart + 365n * DAY
28
29await addClaimScheduleBucketV2(umi, {
30 genesisAccount,
31 baseMint,
32 authority: umi.identity,
33 recipient,
34 bucketIndex,
35 baseTokenAllocation: 100_000_000_000_000n, // 100,000 tokens at 9 decimals
36 claimStartCondition: createTimeAbsoluteCondition(vestingStart),
37 claimSchedule: createClaimSchedule({
38 startTime: vestingStart,
39 endTime: vestingEnd,
40 period: 30n * DAY,
41 cliffTime: vestingStart,
42 cliffAmountBps: 1_000, // 10%
43 }),
44 pausable: true,
45 cancelable: true,
46 cancelableByRecipient: false,
47 transferable: true,
48 transferableByRecipient: false,
49 backendSigner: null,
50 endBehaviors: [],
51}).sendAndConfirm(umi)
52
53console.log('ClaimScheduleBucketV2:', vestingBucket)
Add all other distribution buckets, then call finalizeV2 as described in Genesis setup. Finalization is irreversible.
Claim Vested Project Tokens
claimClaimScheduleV2 transfers every currently vested, unclaimed token to the bucket's stored recipient.
1import {
2 claimClaimScheduleV2,
3 findClaimScheduleBucketV2Pda,
4 genesis,
5} from '@metaplex-foundation/genesis'
6import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
10
11// umi.use(keypairIdentity(yourKeypair))
12
13const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
14const baseMint = publicKey('YOUR_PROJECT_TOKEN_MINT')
15const recipient = publicKey('VESTING_RECIPIENT')
16const [vestingBucket] = findClaimScheduleBucketV2Pda(umi, {
17 genesisAccount,
18 bucketIndex: 0,
19})
20
21await claimClaimScheduleV2(umi, {
22 genesisAccount,
23 bucket: vestingBucket,
24 baseMint,
25 recipient,
26}).sendAndConfirm(umi)
27
28// All currently vested, unclaimed tokens are sent to the stored recipient.
The payer does not have to be the recipient. The instruction creates the recipient's associated token account when needed and can never redirect tokens to the payer.
A claim can return NothingToClaim when no complete vesting period has elapsed since the previous claim. Wait for another period or check the fetched bucket state before sending the transaction.
Claim Schedule Vesting Mechanics
A claim schedule unlocks the cliff allocation independently from the remaining linear allocation.
| Field | Constraint | Effect |
|---|---|---|
startCondition | TimeAbsolute, TimeRelative, or Never | Anchors the linear vesting timeline |
duration | Greater than zero, no longer than 10 years | Defines when the linear allocation is fully vested |
period | Greater than zero and no longer than duration | Makes linear vesting advance in discrete steps |
cliffCondition | TimeAbsolute, TimeRelative, or Never | Unlocks the cliff independently from the linear schedule |
cliffAmountBps | 0 to 10_000 | Assigns 0% to 100% of the allocation to the cliff |
For allocation A and cliff basis points C, the cliff amount is A × C / 10,000. The remaining A - cliffAmount vests linearly in complete period steps over duration.
The cliff does not delay the linear schedule automatically. Set startCondition and cliffCondition to the intended timestamps explicitly; either condition may trigger before, during, or after the other.
Set the cliff no later than startCondition + duration. A successful claim after linear completion fires the bucket's end condition; a later cliff would then be beyond the frozen effective time and could never become claimable.
Claim Gate and Vesting Curve
claimStartCondition gates token withdrawals, while claimSchedule.startCondition controls when the linear allocation accrues.
This separation supports schedules that accrue before claims open. For example, vesting can start on an employment date while claimStartCondition prevents withdrawals until a token generation event.
TimeAbsolute conditions update themselves when a claim checks them. TimeRelative conditions are passive and require triggerConditionsV2 with each referenced bucket passed as a writable remaining account.
1import { genesis, triggerConditionsV2 } from '@metaplex-foundation/genesis'
2import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
3import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
4
5const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
6
7// umi.use(keypairIdentity(yourKeypair))
8
9const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
10const baseMint = publicKey('YOUR_PROJECT_TOKEN_MINT')
11const vestingBucket = publicKey('CLAIM_SCHEDULE_BUCKET_PDA')
12const referenceBucket = publicKey('REFERENCE_BUCKET_PDA')
13
14await triggerConditionsV2(umi, {
15 genesisAccount,
16 bucket: vestingBucket,
17 baseMint,
18})
19 .addRemainingAccounts([
20 { pubkey: referenceBucket, isSigner: false, isWritable: true },
21 ])
22 .sendAndConfirm(umi)
23
24// Eligible TimeRelative conditions on the vesting bucket are now triggered.
Run this permissionless crank after the reference condition is met and before claiming or evaluating the vesting state.
Periodic Linear Unlocks
The period field makes the linear allocation unlock in steps rather than continuously.
With a 365-day duration and a 30-day period, the linear allocation increases after each complete 30-day period. Any rounding remainder becomes claimable when the full duration ends.
Pause-Adjusted Vesting Time
Pausing a bucket stops vesting time, and resuming shifts the effective timeline by the total paused duration.
The bucket records pausedAt and totalSecondsPaused. A cancellation while paused freezes the schedule at pausedAt, so time spent paused cannot increase the vested amount.
Project Vesting Runtime Controls
Runtime controls are disabled by default and must be enabled with policy flags when the bucket is created.
| Policy flag | Authorized role | Instruction | Result |
|---|---|---|---|
pausable | Genesis authority | setClaimSchedulePausedStateV2 | Pauses or resumes vesting accrual |
cancelable | Genesis authority | cancelClaimScheduleBucketV2 | Freezes vesting at the cancellation time |
cancelableByRecipient | Recipient | cancelClaimScheduleBucketV2 | Lets the recipient freeze vesting |
transferable | Genesis authority | transferRecipientClaimScheduleBucketV2 | Changes the vesting recipient |
transferableByRecipient | Recipient | transferRecipientClaimScheduleBucketV2 | Lets the current recipient transfer the allocation |
Enable only the controls required by the project's vesting agreement. Authority cancellation or recipient transfer rights materially change the guarantees provided to the recipient.
Pause and Resume Project Vesting
setClaimSchedulePausedStateV2 pauses or resumes a bucket when pausable was enabled at creation.
1import {
2 genesis,
3 setClaimSchedulePausedStateV2,
4} from '@metaplex-foundation/genesis'
5import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
6import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
7
8const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
9
10// umi.use(keypairIdentity(yourKeypair))
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
13const vestingBucket = publicKey('CLAIM_SCHEDULE_BUCKET_PDA')
14
15await setClaimSchedulePausedStateV2(umi, {
16 genesisAccount,
17 bucket: vestingBucket,
18 signer: umi.identity,
19 paused: true,
20 padding: Array(6).fill(0),
21}).sendAndConfirm(umi)
22
23// Set paused to false and send again to resume vesting.
Set paused: false to resume. Only the Genesis authority can use this control.
Cancel Project Vesting
cancelClaimScheduleBucketV2 freezes vesting but does not remove tokens that were already vested.
1import {
2 cancelClaimScheduleBucketV2,
3 genesis,
4} from '@metaplex-foundation/genesis'
5import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
6import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
7
8const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
9
10// umi.use(keypairIdentity(yourKeypair))
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
13const vestingBucket = publicKey('CLAIM_SCHEDULE_BUCKET_PDA')
14
15await cancelClaimScheduleBucketV2(umi, {
16 genesisAccount,
17 bucket: vestingBucket,
18 signer: umi.identity,
19 padding: Array(7).fill(0),
20}).sendAndConfirm(umi)
21
22// Vesting is frozen, but the recipient can still claim the vested remainder.
The recipient can continue claiming the vested remainder. Unvested tokens stay in Genesis accounting unless a ReallocateBaseTokensOnCancel end behavior is configured and triggered.
Transfer the Project Vesting Recipient
transferRecipientClaimScheduleBucketV2 changes the wallet that receives all future claims.
1import {
2 genesis,
3 transferRecipientClaimScheduleBucketV2,
4} from '@metaplex-foundation/genesis'
5import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
6import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
7
8const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
9
10// umi.use(keypairIdentity(yourKeypair))
11
12const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
13const vestingBucket = publicKey('CLAIM_SCHEDULE_BUCKET_PDA')
14
15await transferRecipientClaimScheduleBucketV2(umi, {
16 genesisAccount,
17 bucket: vestingBucket,
18 signer: umi.identity,
19 newRecipient: publicKey('NEW_RECIPIENT'),
20 padding: Array(7).fill(0),
21}).sendAndConfirm(umi)
22
23// Future claims are sent to the new stored recipient.
The authorized signer depends on whether transferable or transferableByRecipient was enabled.
Reallocating Unvested Tokens After Cancellation
ReallocateBaseTokensOnCancel moves 100% of a canceled bucket's unvested remainder to an UnlockedBucketV2.
Configure the behavior before calling finalizeV2, either in addClaimScheduleBucketV2.endBehaviors or with setClaimScheduleBucketV2Behaviors. The Genesis program rejects behavior configuration after finalization.
1import {
2 genesis,
3 setClaimScheduleBucketV2Behaviors,
4 triggerBehaviorsV2,
5} from '@metaplex-foundation/genesis'
6import { findAssociatedTokenPda, mplToolbox } from '@metaplex-foundation/mpl-toolbox'
7import { keypairIdentity, publicKey } from '@metaplex-foundation/umi'
8import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
9
10const umi = createUmi('https://api.mainnet-beta.solana.com')
11 .use(mplToolbox())
12 .use(genesis())
13
14// umi.use(keypairIdentity(yourKeypair))
15
16const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
17const baseMint = publicKey('YOUR_PROJECT_TOKEN_MINT')
18const quoteMint = publicKey('So11111111111111111111111111111111111111112')
19const vestingBucket = publicKey('CLAIM_SCHEDULE_BUCKET_PDA')
20const destinationBucket = publicKey('UNLOCKED_BUCKET_PDA')
21const [destinationQuoteTokenAccount] = findAssociatedTokenPda(umi, {
22 owner: destinationBucket,
23 mint: quoteMint,
24})
25
26// Configure before finalizeV2.
27await setClaimScheduleBucketV2Behaviors(umi, {
28 genesisAccount,
29 bucket: vestingBucket,
30 authority: umi.identity,
31 padding: Array(3).fill(0),
32 endBehaviors: [
33 {
34 __kind: 'ReallocateBaseTokensOnCancel',
35 processed: false,
36 padding: Array(6).fill(0),
37 destinationBucket,
38 },
39 ],
40}).sendAndConfirm(umi)
41
42// Run after finalization and after cancelClaimScheduleBucketV2 ends the bucket.
43await triggerBehaviorsV2(umi, {
44 genesisAccount,
45 primaryBucket: vestingBucket,
46 baseMint,
47 quoteMint,
48})
49 .addRemainingAccounts([
50 { pubkey: destinationBucket, isSigner: false, isWritable: true },
51 {
52 pubkey: destinationQuoteTokenAccount,
53 isSigner: false,
54 isWritable: true,
55 },
56 ])
57 .sendAndConfirm(umi)
58
59// The unvested remainder is moved to the destination UnlockedBucketV2 balance.
The behavior changes bucket balances, not the original allocation values. It can run only after the claim schedule bucket has ended, and each claim schedule bucket can have at most one cancellation reallocation behavior.
Schedules using TimeRelative start or cliff conditions must have those conditions triggered before cancellation reallocation can calculate the vested amount. Run triggerConditionsV2 with the required reference accounts first.
Updating Project Vesting Before Launch
updateClaimScheduleBucketV2 can replace the allocation, schedule, or claim start condition only before finalization and before vesting has begun.
The bucket rejects updates with ClaimScheduleUpdateForbidden after any tokens have been claimed, after claimStartCondition is met, or after the linear start or cliff condition is met. A TimeRelative condition in any of those three slots also disables updates immediately because the program cannot verify its referenced bucket during an update. Runtime pause, cancellation, and transfer controls use their dedicated instructions instead of the update instruction.
Fetching Project Vesting State
fetchClaimScheduleBucketV2 returns the allocation, claim progress, effective pause state, policies, and end behaviors.
1import {
2 fetchClaimScheduleBucketV2,
3 findClaimScheduleBucketV2Pda,
4 genesis,
5} from '@metaplex-foundation/genesis'
6import { publicKey } from '@metaplex-foundation/umi'
7import { createUmi } from '@metaplex-foundation/umi-bundle-defaults'
8
9const umi = createUmi('https://api.mainnet-beta.solana.com').use(genesis())
10
11const genesisAccount = publicKey('YOUR_GENESIS_ACCOUNT')
12const [vestingBucket] = findClaimScheduleBucketV2Pda(umi, {
13 genesisAccount,
14 bucketIndex: 0,
15})
16
17const account = await fetchClaimScheduleBucketV2(umi, vestingBucket)
18
19console.log('Recipient:', account.recipient)
20console.log('Allocation:', account.bucket.baseTokenAllocation)
21console.log('Remaining balance:', account.bucket.baseTokenBalance)
22console.log('Claimed:', account.amountClaimed)
23console.log('Paused:', account.paused)
24console.log('Total seconds paused:', account.totalSecondsPaused)
The accounting invariant is baseTokenBalance = baseTokenAllocation - amountClaimed until an end behavior reallocates an unvested balance.
ClaimScheduleBucketV2 Account Fields
The ClaimScheduleBucketV2 account stores fixed vesting state followed by a variable-length list of end behaviors.
| Field | Description |
|---|---|
bucket | Shared bucket header containing allocation, remaining balance, mints, index, and fee data |
recipient | Wallet that receives every vested token claim |
amountClaimed | Cumulative tokens transferred to the recipient |
claimSchedule | Cliff and period-based linear vesting curve |
claimStartCondition | Independent gate that must open before claims |
claimEndCondition | Program-owned end condition fired by cancellation or natural completion |
paused | Whether vesting time is currently stopped |
pausedAt | Timestamp at which the current pause began |
totalSecondsPaused | Accumulated pause duration excluded from vesting |
extensions | Runtime policies and optional backend signer |
endBehaviors | Actions available after the bucket ends |
Common Project Vesting Errors
Claim schedule errors identify invalid schedule configuration, unauthorized controls, or actions attempted at the wrong lifecycle stage.
| Error | Cause | Resolution |
|---|---|---|
InvalidClaimSchedulePeriod | period is zero | Use a positive period |
InvalidClaimScheduleDuration | duration is zero or over 10 years | Use a duration from 1 second through 315,360,000 seconds |
ClaimScheduleDurationTooShort | period exceeds duration | Reduce the period or increase the duration |
InvalidClaimScheduleCliffAmount | cliffAmountBps exceeds 10_000 | Use 0 to 10,000 basis points |
NothingToClaim | No new cliff or complete linear period has vested | Wait for the next unlock or inspect bucket state |
ClaimScheduleUpdateForbidden | The claim gate or schedule has begun, tokens were claimed, or a relevant condition is TimeRelative | Configure absolute-time fields before they trigger; relative schedules cannot be updated |
ClaimScheduleUnauthorized | The signer is not allowed to use the control | Use the Genesis authority or recipient required by the enabled policy |
ClaimSchedulePolicyDisabled | The requested pause, cancel, or transfer policy is off | Enable the policy when creating the bucket |
InvalidBackendSigner | A configured backend signer did not authorize the claim | Include the configured backend signer |
ClaimScheduleConditionNotTriggered | Cancellation reallocation depends on unresolved relative conditions | Trigger the relative schedule conditions first |
Quick Reference
Project vesting is available through Genesis V2 and @metaplex-foundation/genesis.
| Item | Value |
|---|---|
| Program | GNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B |
| Tested SDK | @metaplex-foundation/[email protected] |
| Tested Umi compatibility | @metaplex-foundation/umi@^1.4.1 |
| Bucket PDA seeds | "claim_schedule_v2", Genesis account, bucketIndex as u8 |
| Maximum vesting duration | 315,360,000 seconds (10 years) |
| Cliff range | 0 to 10,000 basis points |
| Recipients per bucket | One |
| Bucket creation fee | 0 |
| Devnet validation | Full add, finalize, pause, transfer, claim, cancel, and reallocation flow passed on 2026-08-24 (test account) |
| Source | metaplex-foundation/genesis |
Notes
Project vesting has lifecycle and authorization constraints that should be included in the project's token distribution design.
- Add and configure
ClaimScheduleBucketV2accounts before callingfinalizeV2. - Create one bucket per recipient or independently managed allocation.
- Claims are permissionless unless the optional backend signer extension is configured.
- A backend signer adds claim authorization but cannot redirect a claim away from the current stored recipient.
- Cancellation preserves vested tokens; reclaiming unvested tokens requires
ReallocateBaseTokensOnCancel. - A
Neverschedule permanently locks tokens and is primarily used for locked LP tokens.
FAQ
What is the difference between ClaimScheduleBucketV2 and ClaimSchedule?
ClaimScheduleBucketV2 is a Genesis outflow bucket that holds one recipient's allocation and runtime state. ClaimSchedule is the reusable cliff and linear unlock curve stored inside that bucket.
Does the vesting recipient have to submit each claim?
No. claimClaimScheduleV2 is permissionless, but the program always transfers tokens to the recipient stored on the bucket. If a backend signer extension is configured, that signer must also authorize each claim.
Can a project change a vesting schedule after vesting begins?
No. updateClaimScheduleBucketV2 works only before finalization, before the claim gate or either schedule condition is met, and before any claim. A TimeRelative claim gate, linear start, or cliff also disables updates immediately.
What happens to unvested tokens when a vesting bucket is canceled?
Cancellation freezes vesting and preserves any vested amount for the recipient. If the bucket has a ReallocateBaseTokensOnCancel behavior, anyone can trigger that behavior to move the unvested remainder to an UnlockedBucketV2.
Can one ClaimScheduleBucketV2 vest tokens to multiple recipients?
No. Each bucket has one recipient. Create one ClaimScheduleBucketV2 for each recipient or allocation that needs independent accounting or policy controls.
Glossary
Project vesting terminology distinguishes the bucket account, its embedded schedule, and its lifecycle controls.
| Term | Definition |
|---|---|
| ClaimScheduleBucketV2 | A Genesis outflow bucket that vests one base token allocation to one recipient |
| ClaimSchedule | A reusable cliff and period-based linear token unlock curve |
| Claim gate | The bucket-level claimStartCondition that controls when withdrawals may begin |
| Cliff | A percentage of the total allocation unlocked when an independent condition triggers |
| Period | The interval used to advance linear vesting in discrete steps |
| Effective time | Wall-clock time adjusted to exclude the bucket's paused duration |
| Cancellation reallocation | An end behavior that moves a canceled bucket's unvested remainder to an unlocked bucket |
