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 ClaimSchedule combines 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.

TypePurpose
ClaimScheduleBucketV2Stores one recipient, allocation, claimed amount, schedule, claim gate, pause state, policies, and end behaviors
ClaimScheduleDefines the cliff amount, cliff condition, linear start condition, duration, and unlock period
ClaimScheduleV2ExtensionsStores 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.

addClaimScheduleBucketV2.ts
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.

claimClaimScheduleV2.ts
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.

FieldConstraintEffect
startConditionTimeAbsolute, TimeRelative, or NeverAnchors the linear vesting timeline
durationGreater than zero, no longer than 10 yearsDefines when the linear allocation is fully vested
periodGreater than zero and no longer than durationMakes linear vesting advance in discrete steps
cliffConditionTimeAbsolute, TimeRelative, or NeverUnlocks the cliff independently from the linear schedule
cliffAmountBps0 to 10_000Assigns 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.

triggerConditionsV2.ts
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 flagAuthorized roleInstructionResult
pausableGenesis authoritysetClaimSchedulePausedStateV2Pauses or resumes vesting accrual
cancelableGenesis authoritycancelClaimScheduleBucketV2Freezes vesting at the cancellation time
cancelableByRecipientRecipientcancelClaimScheduleBucketV2Lets the recipient freeze vesting
transferableGenesis authoritytransferRecipientClaimScheduleBucketV2Changes the vesting recipient
transferableByRecipientRecipienttransferRecipientClaimScheduleBucketV2Lets 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.

pauseClaimScheduleBucketV2.ts
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.

cancelClaimScheduleBucketV2.ts
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.

transferClaimScheduleRecipientV2.ts
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.

reallocateClaimScheduleOnCancelV2.ts
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.

fetchClaimScheduleBucketV2.ts
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.

FieldDescription
bucketShared bucket header containing allocation, remaining balance, mints, index, and fee data
recipientWallet that receives every vested token claim
amountClaimedCumulative tokens transferred to the recipient
claimScheduleCliff and period-based linear vesting curve
claimStartConditionIndependent gate that must open before claims
claimEndConditionProgram-owned end condition fired by cancellation or natural completion
pausedWhether vesting time is currently stopped
pausedAtTimestamp at which the current pause began
totalSecondsPausedAccumulated pause duration excluded from vesting
extensionsRuntime policies and optional backend signer
endBehaviorsActions 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.

ErrorCauseResolution
InvalidClaimSchedulePeriodperiod is zeroUse a positive period
InvalidClaimScheduleDurationduration is zero or over 10 yearsUse a duration from 1 second through 315,360,000 seconds
ClaimScheduleDurationTooShortperiod exceeds durationReduce the period or increase the duration
InvalidClaimScheduleCliffAmountcliffAmountBps exceeds 10_000Use 0 to 10,000 basis points
NothingToClaimNo new cliff or complete linear period has vestedWait for the next unlock or inspect bucket state
ClaimScheduleUpdateForbiddenThe claim gate or schedule has begun, tokens were claimed, or a relevant condition is TimeRelativeConfigure absolute-time fields before they trigger; relative schedules cannot be updated
ClaimScheduleUnauthorizedThe signer is not allowed to use the controlUse the Genesis authority or recipient required by the enabled policy
ClaimSchedulePolicyDisabledThe requested pause, cancel, or transfer policy is offEnable the policy when creating the bucket
InvalidBackendSignerA configured backend signer did not authorize the claimInclude the configured backend signer
ClaimScheduleConditionNotTriggeredCancellation reallocation depends on unresolved relative conditionsTrigger the relative schedule conditions first

Quick Reference

Project vesting is available through Genesis V2 and @metaplex-foundation/genesis.

ItemValue
ProgramGNS1S5J5AspKXgpjz6SvKL66kPaKWAhaGRhCqPRxii2B
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 duration315,360,000 seconds (10 years)
Cliff range0 to 10,000 basis points
Recipients per bucketOne
Bucket creation fee0
Devnet validationFull add, finalize, pause, transfer, claim, cancel, and reallocation flow passed on 2026-08-24 (test account)
Sourcemetaplex-foundation/genesis

Notes

Project vesting has lifecycle and authorization constraints that should be included in the project's token distribution design.

  • Add and configure ClaimScheduleBucketV2 accounts before calling finalizeV2.
  • 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 Never schedule 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.

TermDefinition
ClaimScheduleBucketV2A Genesis outflow bucket that vests one base token allocation to one recipient
ClaimScheduleA reusable cliff and period-based linear token unlock curve
Claim gateThe bucket-level claimStartCondition that controls when withdrawals may begin
CliffA percentage of the total allocation unlocked when an independent condition triggers
PeriodThe interval used to advance linear vesting in discrete steps
Effective timeWall-clock time adjusted to exclude the bucket's paused duration
Cancellation reallocationAn end behavior that moves a canceled bucket's unvested remainder to an unlocked bucket