Skip to main content
All requests require an API key via the X-API-Key header. See Authentication for details.
To create streams, the organization attached to the API key must also have permission to create streams. If your organization still does not have that permission enabled, contact the Turtle team to request it.

Overview

POST /v1/streams/ creates a new incentive stream owned by the organization attached to the API key. There are two creation flows:
  • Token-based stream: the API validates the request, stores the pending stream, and returns txParams with the backend-signed authorization that your wallet must submit on-chain to the corresponding StreamFactory.
  • Point-based stream: the API creates the stream immediately and returns txParams: null.
Points are only used for point-based streams. If your stream uses rewardTokenId, you do not need to create or reference a point.
A stream can use both rewardTokenId and targetTokenId on the same chain. The difference is support scope, not a requirement to split them across networks. Reward-token selection is currently supported only for stream creation on 5 networks: Ethereum, Base, Avalanche, BSC, and Sepolia. targetTokenId supports a broader set of chains through Get Tokens, where the lookup uses the decimal EVM chainId.

Supported Stream Types

TypeStrategyRequired behavior
1Fixed RatetotalAmount required for token-based streams
2Fixed APRrewardTokenId and totalAmount required
3Variable RatetotalAmount must be omitted

Create a Token-Based Stream

If you are creating a token-based stream, use Get Tokens to list the supported reward-token UUIDs (rewardTokenId) for the selected chain, or to discover supported target tokens used in strategy customArgs (for example, targetTokenId). For rewardTokenId, choose a token where isAllowedRewardToken is true.
For token-based streams, the totalAmount specified in the request is the exact amount used in the on-chain stream-creation transaction. This amount must already be available in the submitting wallet, as it will be transferred to the smart contract when the stream creation is finalized on-chain.
curl -X POST "https://earn.turtle.xyz/v1/streams/" \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "walletAddress": "0x1111111111111111111111111111111111111111",
    "type": 2,
    "rewardTokenId": "56b0fab0-5c3e-49f6-a0a7-57e38d5ea999",
    "totalAmount": "2500000000000000000000",
    "startTimestamp": "2026-03-20T00:00:00Z",
    "endTimestamp": "2026-04-20T00:00:00Z",
    "customArgs": {
      "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
      "apr": "0.12"
    },
    "adapters": []
  }'

Create a Point-Based Stream

If you are creating a point-based stream and do not yet have a point asset for your organization, use Create Point first. You can list existing organization points with Get Points.
curl -X POST "https://earn.turtle.xyz/v1/streams/" \
  -H "X-API-Key: sk_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "type": 1,
    "pointId": "9a598b70-c6f9-4a1f-9357-3a5823a7ce36",
    "totalAmount": "5000000000000000000000",
    "startTimestamp": "2026-03-20T00:00:00Z",
    "endTimestamp": "2026-04-20T00:00:00Z",
    "customArgs": {
      "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
      "tokensPerUSD": "1000000000000000"
    },
    "adapters": []
  }'

Request Body

walletAddress
string
Admin EVM address for token-based streams. Must be omitted for point-based streams.
type
integer
required
Stream type. Supported values are 1, 2, and 3.
rewardTokenId
uuid
Reward token UUID (use the token id from Get Tokens) for token-based streams. Exactly one of rewardTokenId or pointId must be provided. Choose a token where isAllowedRewardToken is true. Reward-token selection for stream creation is currently limited to Ethereum, Base, Avalanche, BSC, and Sepolia; rewardTokenId will be resolved by the server into the token address and chain used for on-chain creation.
pointId
uuid
Point identifier for point-based streams only. Exactly one of rewardTokenId or pointId must be provided. See Get Points for the Point schema and examples.
totalAmount
string
Total rewards in base units. Required for token-based fixed-rate and fixed-APR streams. Must be omitted for variable-rate streams. For token-based streams, this is the amount that will be sent to the smart contract onchain when the wallet submits the returned txParams, so the wallet must already hold the required balance.
startTimestamp
datetime
required
UTC stream start timestamp. Must align to a 15-minute boundary.
endTimestamp
datetime
Optional UTC stream end timestamp. If provided, it must be at least one hour after startTimestamp and aligned to a 15-minute boundary. For token-based variable-rate streams, it is required.
customArgs
object
required
Strategy-specific configuration object. See customArgs by Type for details.
adapters
array
default:"[]"
Optional adapter configuration array. Each adapter entry must include type and params.

customArgs by Type

Fixed Rate (type = 1)

{
  "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
  "tokensPerUSD": "1000000000000000"
}
  • targetTokenId: supported target token used as the tracked balance source. Resolve it through Get Tokens using the decimal EVM chainId for the target-token network.
  • tokensPerUSD: positive reward-emission coefficient in base units. Specifies the daily number of token/point wei units to grant for every 1,000 USD of provisioned TVL.

Fixed APR (type = 2)

{
  "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
  "apr": "0.12"
}
  • targetTokenId: supported target token used as the tracked balance source. Resolve it through Get Tokens using the decimal EVM chainId for the target-token network.
  • apr: positive decimal APR value.

Variable Rate (type = 3)

{
  "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
  "tokensPerDay": "1000000000000000000"
}
  • targetTokenId: supported target token used as the tracked balance source. Resolve it through Get Tokens using the decimal EVM chainId for the target-token network.
  • tokensPerDay: positive per-day reward budget in base units.

Response Examples

Token-based response

{
  "message": "Successfully generated signature for token-based stream",
  "txParams": {
    "chainId": 1,
    "sender": "0x1111111111111111111111111111111111111111",
    "params": {
      "params": {
        "StreamId": [85, 14, 132, 0, 226, 155, 65, 212, 167, 22, 68, 102, 85, 68, 0, 0],
        "RewardToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "NetTotalAmount": "2500000000000000000000",
        "FeeAmount": "0"
      },
      "deadline": "1773628800",
      "signature": "MEUCIA...base64..."
    }
  },
  "stream": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "chainId": 1,
    "contractAddress": null,
    "userId": null,
    "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
    "admin": "0x1111111111111111111111111111111111111111",
    "type": 2,
    "createdAt": "2026-03-20T00:00:00Z",
    "updatedAt": "2026-03-20T00:00:00Z",
    "startTimestamp": "2026-03-20T00:00:00Z",
    "endTimestamp": "2026-04-20T00:00:00Z",
    "totalAmount": "2500000000000000000000",
    "creationConfirmedAt": null,
    "snapshotComputationPaused": false,
    "merkleTreeComputationPaused": false,
    "hashCommitmentPaused": false,
    "claimPaused": false,
    "customArgs": {
      "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
      "apr": "0.12"
    },
    "adapters": [],
    "point": null,
    "strategy": "Fixed APR",
    "lastSnapshot": null,
    "committedSnapshot": null,
    "rewardToken": {
      "id": "56b0fab0-5c3e-49f6-a0a7-57e38d5ea999",
      "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      "name": "USD Coin",
      "symbol": "USDC",
      "decimals": 6,
      "chainId": 1,
      "logoUrl": "https://cdn.example.com/tokens/usdc.png",
      "isAllowedRewardToken": true
    }
  }
}

Point-based response

{
  "message": "Successfully created point-based stream",
  "txParams": null,
  "stream": {
    "id": "550e8400-e29b-41d4-a716-446655440001",
    "chainId": null,
    "contractAddress": null,
    "userId": null,
    "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
    "admin": null,
    "type": 1,
    "createdAt": "2026-03-20T00:00:00Z",
    "updatedAt": "2026-03-20T00:00:00Z",
    "startTimestamp": "2026-03-20T00:00:00Z",
    "endTimestamp": "2026-04-20T00:00:00Z",
    "totalAmount": "5000000000000000000000",
    "creationConfirmedAt": "2026-03-20T00:00:00Z",
    "snapshotComputationPaused": false,
    "merkleTreeComputationPaused": false,
    "hashCommitmentPaused": false,
    "claimPaused": false,
    "customArgs": {
      "targetTokenId": "8cc2ed9d-bd59-42fd-9df5-329fa22497b6",
      "tokensPerUSD": "1000000000000000"
    },
    "adapters": [],
    "point": {
      "id": "9a598b70-c6f9-4a1f-9357-3a5823a7ce36",
      "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
      "symbol": "POINT",
      "name": "Partner Points",
      "decimals": 18,
      "logoUrl": "https://cdn.example.com/points/partner-points.png",
      "createdAt": "2026-03-01T00:00:00Z",
      "updatedAt": "2026-03-01T00:00:00Z"
    },
    "strategy": "Fixed Rate",
    "lastSnapshot": null,
    "committedSnapshot": null,
    "rewardToken": null
  }
}

Response Fields

message
string
required
Status message describing the creation path used.
txParams
object | null
required
On-chain creation payload for token-based streams. Submit it to the corresponding StreamFactory on txParams.chainId. null for point-based streams.
txParams.chainId
integer
required
Chain where the stream-factory transaction must be submitted.
txParams.sender
string
required
Wallet expected to submit the transaction.
txParams.params.params.StreamId
number[]
required
16-byte stream ID encoded as a byte array for the stream factory call.
txParams.params.params.RewardToken
string
required
Reward token contract address.
txParams.params.params.NetTotalAmount
string
required
Streamed amount in decimal-string form to preserve precision.
txParams.params.params.FeeAmount
string
required
Creation fee amount in decimal-string form.
txParams.params.deadline
string
required
Unix timestamp deadline for the signed payload.
txParams.params.signature
string
required
EIP-712 signature bytes serialized as base64 in JSON.
stream
Stream
required
Persisted stream record created by the request. It uses the same public stream schema returned by Get Streams.
stream.adapters
AdapterConfig[]
required
Persisted adapter configuration array. Each item contains a type string and a params object.
stream.rewardToken
SupportedToken | null
required
Reward-token metadata for token-based streams. This uses the same token shape returned by Get Tokens.

Broadcast the Token-Based Transaction

For token-based streams, the backend does not return a fully serialized raw transaction. Instead, it returns a signed authorization payload in txParams that your wallet must use to call createStream on the StreamFactory for the target chain. The wallet that sends the transaction must:
  • match txParams.sender
  • be connected to txParams.chainId
  • approve the StreamFactory to transfer the reward token amount needed for NetTotalAmount + FeeAmount

StreamFactory addresses by chain

chainIdNetworkStreamFactory
1Ethereum0xf44399a74ee5ddef7fa3d064cf66b011ee4a6cae
56BSC0x298d2967588b5c93a137ce1a05d0b8cfffb3c120
43114Avalanche0x4559605e3003fda8c059e14af4f16ba9a004335a
8453Base0x4559605e3003fda8c059e14af4f16ba9a004335a
11155111Sepolia0xdfdff939d728585ce8a2cf2d4166f043d917d8d2

TypeScript example

import { ethers } from 'ethers';

const STREAM_FACTORY_BY_CHAIN: Record<number, string> = {
  1: '0xf44399a74ee5ddef7fa3d064cf66b011ee4a6cae',
  56: '0x298d2967588b5c93a137ce1a05d0b8cfffb3c120',
  43114: '0x4559605e3003fda8c059e14af4f16ba9a004335a',
  8453: '0x4559605e3003fda8c059e14af4f16ba9a004335a',
  11155111: '0xdfdff939d728585ce8a2cf2d4166f043d917d8d2',
};

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) external returns (bool)',
];

const STREAM_FACTORY_ABI = [
  'function createStream((bytes16 streamId,address rewardToken,uint256 netTotalAmount,uint256 feeAmount) params,uint40 deadline,bytes signature) external returns (address stream)',
];

function byteArrayToBytes16(value: number[]): string {
  if (value.length !== 16) {
    throw new Error('Expected txParams.params.params.StreamId to contain 16 bytes');
  }

  return ethers.hexlify(Uint8Array.from(value));
}

function base64ToBytes(value: string): Uint8Array {
  return Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
}

const response = await fetch('https://earn.turtle.xyz/v1/streams/', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.TURTLE_SECRET_KEY!,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    walletAddress: '0x1111111111111111111111111111111111111111',
    type: 2,
    rewardTokenId: '56b0fab0-5c3e-49f6-a0a7-57e38d5ea999',
    totalAmount: '2500000000000000000000',
    startTimestamp: '2026-03-20T00:00:00Z',
    endTimestamp: '2026-04-20T00:00:00Z',
    customArgs: {
      targetTokenId: '8cc2ed9d-bd59-42fd-9df5-329fa22497b6',
      apr: '0.12',
    },
    adapters: [],
  }),
});

const { txParams } = await response.json();

if (!txParams) {
  throw new Error('Expected txParams for a token-based stream');
}

const streamFactoryAddress = STREAM_FACTORY_BY_CHAIN[txParams.chainId];

if (!streamFactoryAddress) {
  throw new Error(`Unsupported StreamFactory for chainId ${txParams.chainId}`);
}

const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);

const signer = await provider.getSigner();
const signerAddress = await signer.getAddress();
const connectedChainId = Number((await provider.getNetwork()).chainId);

if (signerAddress.toLowerCase() !== txParams.sender.toLowerCase()) {
  throw new Error(`Connected wallet ${signerAddress} does not match txParams.sender ${txParams.sender}`);
}

if (connectedChainId !== txParams.chainId) {
  throw new Error(`Connected chain ${connectedChainId} does not match txParams.chainId ${txParams.chainId}`);
}

const requiredAllowance =
  BigInt(txParams.params.params.NetTotalAmount) + BigInt(txParams.params.params.FeeAmount);

const rewardToken = new ethers.Contract(
  txParams.params.params.RewardToken,
  ERC20_ABI,
  signer,
);

const approveTx = await rewardToken.approve(streamFactoryAddress, requiredAllowance);
await approveTx.wait();

const streamFactory = new ethers.Contract(
  streamFactoryAddress,
  STREAM_FACTORY_ABI,
  signer,
);

const createStreamTx = await streamFactory.createStream(
  {
    streamId: byteArrayToBytes16(txParams.params.params.StreamId),
    rewardToken: txParams.params.params.RewardToken,
    netTotalAmount: txParams.params.params.NetTotalAmount,
    feeAmount: txParams.params.params.FeeAmount,
  },
  txParams.params.deadline,
  base64ToBytes(txParams.params.signature),
);

const receipt = await createStreamTx.wait();

console.log('Stream creation tx hash:', receipt?.hash);

Operational Notes

The endpoint does not submit the transaction to the chain. It returns the payload required to finalize creation through the StreamFactory contract. See Broadcast the Token-Based Transaction for the chain addresses and a TypeScript example. When the wallet submits that transaction, the configured totalAmount is part of the on-chain flow, so the wallet must already hold those funds.
This endpoint requires two things at the same time: a valid X-API-Key header and the organization:incentivize:streams:create permission on the organization attached to that key. If the organization has not been granted that permission, stream creation will be rejected.
Point-based streams do not require an on-chain deployment step, so txParams is null and the stream is created directly in Turtle’s backend. In that case, the returned stream is already confirmed.
For token-based streams, the response already includes a persisted stream object, but it is still pending until the wallet broadcasts the returned txParams to the StreamFactory. Until that happens, fields such as stream.contractAddress and stream.creationConfirmedAt can remain null.
startTimestamp and endTimestamp must be aligned to 15-minute intervals such as 00:00, 00:15, 00:30, or 00:45 UTC.
rewardToken and pointId are mutually exclusive. You must specify exactly one of them:
  • rewardTokenId for token-based streams
  • pointId for point-based streams
Do not provide both, and do not omit both.
If you are creating a stream with rewardTokenId, you do not need a point and must not send pointId. Points only apply when the reward source is organization-defined points.

Error Handling

Status Code: 401 Unauthorized
{
  "error": "Invalid API key"
}
Solution: Pass a valid X-API-Key header.
Status Code: 403 Forbidden
{
  "error": {
    "status": "PERMISSION_DENIED",
    "error": "permission denied"
  }
}
Solution: Use an API key associated with an organization that has the organization:incentivize:streams:create permission.
Status Code: 400 Bad Request
{
  "error": {
    "status": "INVALID_ARGUMENT",
    "error": "exactly one between RewardToken and PointID must be provided"
  }
}
Common causes:
  • unsupported type
  • invalid customArgs for the chosen type
  • timestamps not aligned to 15-minute boundaries
  • totalAmount present for variable-rate streams
  • walletAddress or rewardTokenId missing for token-based streams
Status Code: 500 Internal Server ErrorSolution: Retry the request and contact Turtle if the issue persists.