Guide: Create a Vesting Stream
End-to-end examples for simple vesting, initial allocation, and class-based creation.
Import and set up
import { Connection, Keypair } from "@solana/web3.js";
import BN from "bn.js";
import { buildTransaction, createVesting, execute, sign } from "@streamflow/stream/solana/api";
import { pk } from "@streamflow/common";
import fs from "node:fs";
const secret = JSON.parse(fs.readFileSync(process.env.KEYPAIR_PATH!, "utf-8"));
const invoker = Keypair.fromSecretKey(Uint8Array.from(secret));
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const env = {
connection,
programId: pk("HqDGZjaVRXJ9MGRQEw7qDc2rAr6iH1n1kAQdCZaCMfMZ"),
};Phase 1 - Get instructions
const start = Math.floor(Date.now() / 1000) + 10; // 10 seconds from now
const result = await createVesting(
{
recipient: "RecipientWalletAddress...",
tokenId: "TokenMintAddress...",
amount: new BN(3_600_000_000), // 3600 tokens at 6 decimals
start,
period: 86400, // release daily
duration: 365 * 86400, // over 1 year
cliffAmount: new BN(0), // no upfront release
name: "Employee Vesting",
cancelableBySender: true,
canTopup: true,
transferableBySender: true,
},
invoker,
{ ...env, isNative: false },
);
const streamId = result.metadataPubKey!.toBase58();Phases 2 and 3 - Build, sign, and execute
const built = await buildTransaction(
result.instructions,
{ feePayer: invoker.publicKey },
env,
);
await sign(built.transaction, [invoker, ...(result.signers ?? [])]);
const signature = await execute(built, env);
console.log("Stream ID:", streamId);
console.log("Transaction:", signature);Providing initialAllocation creates two streams atomically: the main vesting stream and a separate stream for the upfront allocation. The return type changes to BatchInstructionResult, and you must send a setup transaction followed by one transaction per stream.
The allocation stream uses internal parameters specifically chosen to avoid on-chain lock classification. These are set automatically - you only need to provide the amount.
Phase 1 - Get instructions (returns BatchInstructionResult)
const start = Math.floor(Date.now() / 1000) + 10;
const result = await createVesting(
{
recipient: "RecipientWalletAddress...",
tokenId: "TokenMintAddress...",
amount: new BN(3_600_000_000), // 3600 tokens at 6 decimals
start,
period: 86400,
duration: 365 * 86400,
name: "Vest + Allocation",
cancelableBySender: true,
initialAllocation: { amount: new BN(360_000_000) }, // 10% upfront
},
invoker,
{ ...env, isNative: false },
);
// result: { setupInstructions, creationBatches }
// creationBatches[0] - main vesting stream
// creationBatches[1] - allocation streamSend setup transaction first
Setup creates associated token accounts and wraps SOL if needed. It must confirm before the creation batches are sent.
if (result.setupInstructions.length > 0) {
const setupBuilt = await buildTransaction(
result.setupInstructions,
{ feePayer: invoker.publicKey },
env,
);
await sign(setupBuilt.transaction, [invoker]);
await execute(setupBuilt, env);
}Send one transaction per creation batch
Each batch has its own metadata keypair and must be submitted as a separate transaction.
for (const batch of result.creationBatches) {
const batchBuilt = await buildTransaction(
batch.instructions,
{ feePayer: invoker.publicKey },
env,
);
await sign(batchBuilt.transaction, [invoker, ...(batch.signers ?? [])]);
const sig = await execute(batchBuilt, env);
console.log("Stream:", batch.metadataPubKey.toBase58(), "Tx:", sig);
}client.create() handles building, signing, and submission in a single call. For the initialAllocation pattern, use the composable API instead - it returns a BatchInstructionResult giving you control over each transaction in the sequence.
Import and initialize
import { SolanaStreamClient, ICluster } from "@streamflow/stream";
import { buildVestingParams } from "@streamflow/stream/solana/api";
import { Keypair } from "@solana/web3.js";
import BN from "bn.js";
import fs from "node:fs";
const client = new SolanaStreamClient({
clusterUrl: "https://api.devnet.solana.com",
cluster: ICluster.Devnet,
});
const secret = JSON.parse(fs.readFileSync(process.env.KEYPAIR_PATH!, "utf-8"));
const invoker = Keypair.fromSecretKey(Uint8Array.from(secret));Create the vesting stream
buildVestingParams resolves duration or endDate, computes amountPerPeriod via ceiling division, and fills in protocol defaults before passing to the client.
const start = Math.floor(Date.now() / 1000) + 10;
const { txId, metadataId } = await client.create(
buildVestingParams({
recipient: "RecipientWalletAddress...",
tokenId: "TokenMintAddress...",
amount: new BN(3_600_000_000), // 3600 tokens at 6 decimals
start,
period: 86400, // daily release
duration: 365 * 86400, // 1 year
cliffAmount: new BN(0), // no cliff
name: "Employee Vesting",
cancelableBySender: true,
canTopup: true,
transferableBySender: true,
}),
{
sender: invoker,
isNative: false,
},
);
console.log("Stream ID:", metadataId);
console.log("Transaction:", txId);Fine-grained control (optional)
Use prepareCreateInstructions() to get the raw instructions without submitting:
const { ixs, metadata, metadataPubKey } = await client.prepareCreateInstructions(
buildVestingParams({
recipient: "RecipientWalletAddress...",
tokenId: "TokenMintAddress...",
amount: new BN(3_600_000_000),
start,
period: 86400,
duration: 365 * 86400,
name: "Employee Vesting",
}),
{
sender: { publicKey: invoker.publicKey },
isNative: false,
},
);
// ixs: TransactionInstruction[] - compose into your own transaction
// metadata: Keypair | undefined - co-sign if present
// metadataPubKey: PublicKey - the stream ID (call .toBase58() to get the string)Query stream state
After creation, use getOne() to read stream state, or get() to list all streams for a wallet:
import { StreamDirection, StreamType } from "@streamflow/stream";
// Fetch a single stream
const stream = await client.getOne({ id: metadataId });
console.log("Recipient:", stream.recipient);
console.log("Deposited:", stream.depositedAmount.toString());
console.log("Withdrawn:", stream.withdrawnAmount.toString());
// List all outgoing vesting streams for a sender
const streams = await client.get({
address: invoker.publicKey.toBase58(),
direction: StreamDirection.Outgoing,
type: StreamType.Vesting,
});
for (const [id, stream] of streams) {
console.log(id, "->", stream.recipient, stream.depositedAmount.toString());
}