| name | jb-bendystraw |
| description | Bendystraw GraphQL API reference for querying Juicebox project data across all chains. Get project stats, payments, token holders, loans, NFT tiers, unified activity feeds, historical snapshots, and cross-chain aggregations. |
Bendystraw: Cross-Chain Juicebox Data API
Bendystraw is a GraphQL indexer for Juicebox V5 events across all supported chains. It aggregates data and provides unified cross-chain queries for projects, payments, token holders, and NFTs.
API Base URLs
Production: https://bendystraw.xyz/{API_KEY}/graphql
Testnet: https://testnet.bendystraw.xyz/{API_KEY}/graphql
Playground: https://bendystraw.xyz (browser-based GraphQL explorer)
Authentication
API key required. Contact @peripheralist on Twitter/X to get one.
const response = await fetch(`https://bendystraw.xyz/${API_KEY}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: '...',
variables: { ... }
})
});
Important: Never expose API keys in frontend code. Use a server-side proxy.
Supported Chains
| Chain | Chain ID | Network |
|---|
| Ethereum | 1 | Mainnet |
| Optimism | 10 | Mainnet |
| Base | 8453 | Mainnet |
| Arbitrum | 42161 | Mainnet |
| Sepolia | 11155111 | Testnet |
GraphQL Schema Reference
Project Entity
type Project {
id: String!
projectId: Int!
chainId: Int!
version: Int!
handle: String
name: String
description: String
logoUri: String
infoUri: String
owner: String!
deployer: String
balance: String!
volume: String!
volumeUsd: String
redeemVolume: String!
redeemVolumeUsd: String
String
String
String
Int
Int
Int
Int
Float
String
Int
String
Int
Int
SuckerGroup Entity (Omnichain Projects)
type SuckerGroup {
id: String!
projects: [String!]!
volume: String!
volumeUsd: String
balance: String!
tokenSupply: String!
paymentsCount: Int!
contributorsCount: Int!
projects_rel: [Project!]!
}
Participant Entity (Token Holders)
type Participant {
id: String!
address: String!
projectId: Int!
chainId: Int!
balance: String!
creditBalance: String!
erc20Balance: String!
volume: String!
volumeUsd: String
paymentsCount: Int!
redeemCount: Int!
lastPaidAt: Int
firstPaidAt: Int
}
PayEvent Entity
type PayEvent {
id: String!
projectId: Int!
chainId: Int!
rulesetId: Int!
txHash: String!
timestamp: Int!
logIndex: Int!
blockNumber: Int!
from: String!
beneficiary: String!
amount: String!
amountUsd: String
distributionFromPayAmount: String!
newlyIssuedTokenCount: String!
beneficiaryTokenCount: String!
memo String
String
CashOutEvent Entity
type CashOutEvent {
id: String!
projectId: Int!
chainId: Int!
rulesetId: Int!
txHash: String!
timestamp: Int!
holder: String!
beneficiary: String!
cashOutCount: String!
reclaimAmount: String!
reclaimAmountUsd: String
metadata: String
}
NFT Entity
type NFT {
id: String!
tokenId: Int!
projectId: Int!
chainId: Int!
hook: String!
tierId: Int!
tierCategory: Int
owner: String!
createdAt: Int!
tokenUri: String
}
ActivityEvent Entity (Unified Activity Feed)
A polymorphic event type that provides a unified view of all project activity. Query this instead of individual event types when building activity feeds.
type ActivityEvent {
id: String!
chainId: Int!
projectId: Int!
suckerGroupId: String
version: Int!
txHash: String!
timestamp: Int!
from: String!
type: ActivityEventType!
payEvent: PayEvent
cashOutTokensEvent: CashOutTokensEvent
mintNftEvent: MintNftEvent
sendPayoutsEvent: SendPayoutsEvent
sendPayoutToSplitEvent: SendPayoutToSplitEvent
borrowLoanEvent: BorrowLoanEvent
repayLoanEvent: RepayLoanEvent
liquidateLoanEvent: LiquidateLoanEvent
deployErc20Event: DeployErc20Event
burnEvent BurnEvent
MintTokensEvent
ProjectCreateEvent
AddToBalanceEvent
UseAllowanceEvent
DecorateBannyEvent
Project
SuckerGroup
ActivityEventType
payEvent
cashOutTokensEvent
mintNftEvent
sendPayoutsEvent
sendPayoutToSplitEvent
borrowLoanEvent
repayLoanEvent
liquidateLoanEvent
reallocateLoanEvent
deployErc20Event
burnEvent
mintTokensEvent
manualMintTokensEvent
manualBurnEvent
autoIssueEvent
projectCreateEvent
addToBalanceEvent
useAllowanceEvent
sendReservedTokensToSplitEvent
sendReservedTokensToSplitsEvent
decorateBannyEvent
Loan Entity (RevLoans)
Active loan state from the RevLoans protocol.
type Loan {
id: BigInt!
projectId: Int!
chainId: Int!
version: Int!
createdAt: Int!
borrowAmount: BigInt!
collateral: BigInt!
sourceFeeAmount: BigInt!
prepaidDuration: Int!
prepaidFeePercent: Int!
beneficiary: String!
owner: String!
token: String!
String
String
Project
Participant
Wallet
Wallet Entity
Wallet-level aggregation across all project participations.
type Wallet {
address: String!
volume: BigInt!
volumeUsd: BigInt!
lastPaidTimestamp: Int
participants: ParticipantPage
nfts: NFTPage
}
NFTTier Entity
NFT tier configuration with pricing and supply.
type NFTTier {
chainId: Int!
projectId: Int!
version: Int!
tierId: Int!
price: BigInt!
initialSupply: Int!
remainingSupply: Int!
allowOwnerMint: Boolean!
cannotBeRemoved: Boolean!
transfersPausable: Boolean!
votingUnits: BigInt!
category: Int
reserveFrequency: Int
reserveBeneficiary String
String
String
JSON
String
Int
NFTHook
NFTPage
Project
NFTHook Entity
721 hook contract configuration.
type NFTHook {
chainId: Int!
projectId: Int!
version: Int!
createdAt: Int!
address: String!
name: String
symbol: String
nfts: NFTPage
nftTiers: NFTTierPage
project: Project
}
ProjectMoment Entity (Historical Snapshots)
Point-in-time snapshots of project state. Useful for historical charts and analytics.
type ProjectMoment {
projectId: Int!
chainId: Int!
version: Int!
block: Int!
timestamp: Int!
volume: BigInt!
volumeUsd: BigInt!
balance: BigInt!
trendingScore: BigInt!
project: Project
}
SuckerGroupMoment Entity
Point-in-time snapshots of cross-chain aggregated state.
type SuckerGroupMoment {
suckerGroupId: String!
block: Int!
timestamp: Int!
volume: BigInt!
volumeUsd: BigInt!
balance: BigInt!
tokenSupply: BigInt!
suckerGroup: SuckerGroup
}
SuckerTransaction Entity (Cross-Chain Bridging)
Token bridging transactions between chains via suckers.
type SuckerTransaction {
index: Int!
projectId: Int!
chainId: Int!
version: Int!
suckerGroupId: String!
createdAt: Int!
token: String!
sucker: String!
peer: String!
peerChainId: Int!
beneficiary: String!
projectTokenCount: BigInt!
terminalTokenAmount: BigInt!
String
SuckerTransactionStatus
SuckerGroup
SuckerTransactionStatus
pending
completed
failed
SendPayoutsEvent Entity
Payout distribution events from the terminal.
type SendPayoutsEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
logIndex: Int!
caller: String!
from: String!
rulesetId: Int!
rulesetCycleNumber: Int!
amount: BigInt!
amountUsd: BigInt!
amountPaidOut: BigInt!
amountPaidOutUsd BigInt
BigInt
BigInt
BigInt
Project
UseAllowanceEvent Entity
Surplus allowance usage events.
type UseAllowanceEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
txHash: String!
timestamp: Int!
caller: String!
beneficiary: String!
rulesetId: Int!
rulesetCycleNumber: Int!
amount: BigInt!
amountUsd: BigInt!
netAmount: BigInt!
netAmountUsd: BigInt!
project: Project
}
PermissionHolder Entity
Operator permissions granted to accounts.
type PermissionHolder {
chainId: Int!
projectId: Int!
version: Int!
account: String!
operator: String!
permissions: [Int!]!
isRevnetOperator: Boolean!
project: Project
}
BorrowLoanEvent Entity
Loan creation events from RevLoans.
type BorrowLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
borrowAmount: BigInt!
collateral: BigInt!
sourceFeeAmount: BigInt!
prepaidDuration: Int!
prepaidFeePercent: Int!
beneficiary String
String
String
Project
RepayLoanEvent Entity
Loan repayment events.
type RepayLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
loanId: BigInt!
paidOffLoanId: BigInt
repayBorrowAmount: BigInt!
collateralCountToReturn: BigInt!
project: Project
}
LiquidateLoanEvent Entity
Loan liquidation events.
type LiquidateLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
borrowAmount: BigInt!
collateral: BigInt!
project: Project
}
ReallocateLoanEvent Entity
Loan reallocation events (moving collateral between loans).
type ReallocateLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
loanId: BigInt!
reallocatedLoanId: BigInt!
removedCollateralCount: BigInt!
project: Project
}
BurnEvent Entity
Token burn events (from cash outs).
type BurnEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
from: String!
amount: BigInt!
creditAmount: BigInt!
erc20Amount: BigInt!
project: Project
}
MintTokensEvent Entity
Token minting events (from payments).
type MintTokensEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
beneficiary: String!
beneficiaryTokenCount: BigInt!
reservedPercent: BigInt!
tokenCount: BigInt!
memo: String
project Project
ManualMintTokensEvent Entity
Manual token minting by project owner.
type ManualMintTokensEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
beneficiary: String!
beneficiaryTokenCount: BigInt!
reservedPercent: BigInt!
tokenCount: BigInt!
memo: String
project: Project
}
ManualBurnEvent Entity
Manual token burning.
type ManualBurnEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
from: String!
amount: BigInt!
creditAmount: BigInt!
erc20Amount: BigInt!
project: Project
}
MintNftEvent Entity
NFT minting events.
type MintNftEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
hook: String!
beneficiary: String!
tierId: Int!
tokenId: BigInt!
totalAmountPaid: BigInt!
Project
NFTTier
NFT
DeployErc20Event Entity
ERC20 token deployment events.
type DeployErc20Event {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
symbol: String!
name: String!
token: String!
project: Project
}
ProjectCreateEvent Entity
Project creation events.
type ProjectCreateEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
project: Project
}
AddToBalanceEvent Entity
Direct balance addition events.
type AddToBalanceEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
amount: BigInt!
memo: String
metadata: String
returnedFees: BigInt
project: Project
}
SendPayoutToSplitEvent Entity
Individual split payout events.
type SendPayoutToSplitEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
rulesetId: Int!
group: BigInt!
beneficiary: String!
splitProjectId: Int
hook: String
amount: BigInt!
BigInt
BigInt
Int
BigInt
Boolean
Project
SendReservedTokensToSplitEvent Entity
Reserved token distribution to individual split.
type SendReservedTokensToSplitEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
rulesetId: Int!
groupId: BigInt!
beneficiary: String!
splitProjectId: Int
hook: String
tokenCount: BigInt!
percent: Int!
lockedUntil: BigInt
Boolean
Project
SendReservedTokensToSplitsEvent Entity
Batch reserved token distribution event.
type SendReservedTokensToSplitsEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
rulesetId: Int!
rulesetCycleNumber: Int!
owner: String!
tokenCount: BigInt!
leftoverAmount: BigInt!
project: Project
AutoIssueEvent Entity
Auto-issuance events (revnet stage transitions).
type AutoIssueEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
stageId: BigInt!
beneficiary: String!
count: BigInt!
project: Project
}
StoreAutoIssuanceAmountEvent Entity
Auto-issuance configuration events.
type StoreAutoIssuanceAmountEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
stageId: BigInt!
beneficiary: String!
count: BigInt!
project: Project
}
DecorateBannyEvent Entity
Banny NFT decoration events.
type DecorateBannyEvent {
id: String!
chainId: Int!
version: Int!
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
bannyBodyId: BigInt!
outfitIds: [BigInt!]!
backgroundId: BigInt
tokenUri: String
tokenUriMetadata: JSON
bannyNft: NFT
}
CashOutTaxSnapshot Entity
Historical cash-out tax rate snapshots.
type CashOutTaxSnapshot {
chainId: Int!
projectId: Int!
suckerGroupId: String
version: Int!
start: BigInt!
duration: BigInt!
rulesetId: BigInt!
cashOutTax: Int!
}
ParticipantSnapshot Entity (GraphQL)
Historical participant balance snapshots via GraphQL (alternative to REST endpoint).
type ParticipantSnapshot {
chainId: Int!
projectId: Int!
suckerGroupId: String
version: Int!
block: Int!
timestamp: Int!
address: String!
balance: BigInt!
creditBalance: BigInt!
erc20Balance: BigInt!
volume: BigInt!
volumeUsd: BigInt!
}
Critical Concepts
Project Identity
A Juicebox project is uniquely identified by three fields: projectId + chainId + version.
This is crucial because:
- V4 and V5 are completely different protocols. Project #64 on Ethereum V4 is NOT the same project as Project #64 on Ethereum V5.
- The same projectId can exist on multiple chains (via suckers/omnichain), but those ARE the same project.
- Always include
version when querying or displaying projects.
const groupKey = `${project.projectId}-${project.chainId}`;
const groupKey = `${project.projectId}-v${project.version}`;
Multi-Chain Grouping
When displaying "top projects" or aggregating stats:
- Same projectId + version across chains → Group together (same project via suckers)
- Same projectId, different version → Keep separate (completely different projects)
const grouped = new Map();
for (const project of projects) {
const groupKey = `${project.projectId}-v${project.version || 4}`;
const existing = grouped.get(groupKey);
if (existing) {
if (!existing.chainIds.includes(project.chainId)) {
existing.chainIds.push(project.chainId);
}
existing.totalVolumeUsd += parseFloat(project.volumeUsd || '0');
} else {
grouped.set(groupKey, {
...project,
chainIds: [project.chainId],
totalVolumeUsd: parseFloat(project.volumeUsd || '0')
});
}
}
USD Value Formatting
The volumeUsd, amountUsd, and similar fields use 18 decimal format (like wei). You must convert properly:
function formatVolumeUsd(volumeUsd) {
if (!volumeUsd || volumeUsd === '0') return '$0';
try {
const raw = BigInt(volumeUsd.split('.')[0]);
const usd = Number(raw / BigInt(1e12)) / 1e6;
if (usd >= 1_000_000) return `$${(usd / 1_000_000).toFixed(1)}M`;
if (usd >= 1_000) return `$${(usd / 1_000).toFixed(1)}k`;
if (usd >= 1) return `$${usd.toFixed(0)}`;
return `$${usd.toFixed(2)}`;
} catch {
return '$0';
}
}
Warning: Do NOT use parseFloat() directly on volumeUsd for large values—JavaScript loses precision beyond ~15 digits.
Filtering by Version
When querying projects, filter by version to avoid mixing V4 and V5 data:
query V5Projects($limit: Int!) {
projects(
where: { version: 5 }
orderBy: "volumeUsd"
orderDirection: "desc"
limit: $limit
) {
items {
projectId
chainId
version
name
volumeUsd
}
}
}
To display both versions, query them separately and handle grouping in your application.
Query Examples
Get Single Project
query GetProject($projectId: Int!, $chainId: Int!) {
project(projectId: $projectId, chainId: $chainId) {
id
name
handle
owner
balance
volume
volumeUsd
tokenSupply
paymentsCount
contributorsCount
suckerGroupId
}
}
Get Participant (Token Holder)
query GetParticipant($projectId: Int!, $chainId: Int!, $address: String!) {
participant(projectId: $projectId, chainId: $chainId, address: $address) {
balance
creditBalance
erc20Balance
volume
volumeUsd
paymentsCount
}
}
Get Sucker Group (Omnichain Totals)
query GetSuckerGroup($id: String!) {
suckerGroup(id: $id) {
id
volume
volumeUsd
balance
tokenSupply
paymentsCount
contributorsCount
projects_rel {
projectId
chainId
name
balance
volume
}
}
}
List Projects
query ListProjects($chainId: Int, $version: Int, $limit: Int!, $offset: Int!) {
projects(
where: { chainId: $chainId, version: $version }
orderBy: "volumeUsd"
orderDirection: "desc"
limit: $limit
offset: $offset
) {
items {
projectId
chainId
version
name
handle
volumeUsd
balance
paymentsCount
}
totalCount
}
}
List Recent Payments
query ListPayments($projectId: Int!, $chainId: Int!, $limit: Int!) {
payEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
from
beneficiary
amount
amountUsd
memo
newlyIssuedTokenCount
}
}
}
List Top Token Holders
query ListParticipants($projectId: Int!, $chainId: Int!, $limit: Int!) {
participants(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "balance"
orderDirection: "desc"
limit: $limit
) {
items {
address
balance
creditBalance
erc20Balance
volume
paymentsCount
}
totalCount
}
}
Get Trending Projects
query TrendingProjects($limit: Int!) {
projects(
orderBy: "trendingScore"
orderDirection: "desc"
limit: $limit
) {
items {
projectId
chainId
name
handle
trendingScore
trendingVolume
trendingPaymentsCount
}
}
}
List Cash Out Events
query ListCashOuts($projectId: Int!, $chainId: Int!, $limit: Int!) {
cashOutEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
holder
beneficiary
cashOutCount
reclaimAmount
reclaimAmountUsd
}
}
}
List NFTs for Project
query ListNFTs($projectId: Int!, $chainId: Int!, $limit: Int!) {
nfts(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "createdAt"
orderDirection: "desc"
limit: $limit
) {
items {
tokenId
tierId
tierCategory
owner
createdAt
tokenUri
}
}
}
Get Unified Activity Feed
The most powerful query for building activity feeds. Returns all event types in a single query.
query GetActivityFeed($projectId: Int!, $chainId: Int!, $limit: Int!) {
activityEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
id
timestamp
txHash
from
type
payEvent {
amount
amountUsd
beneficiary
memo
}
cashOutTokensEvent {
cashOutCount
reclaimAmount
holder
}
mintNftEvent {
tierId
tokenId
}
sendPayoutsEvent {
amount
amountPaidOut
fee
}
borrowLoanEvent {
borrowAmount
collateral
}
}
Get Omnichain Activity Feed
Query activity across all chains for a sucker group.
query GetOmnichainActivity($suckerGroupId: String!, $limit: Int!) {
activityEvents(
where: { suckerGroupId: $suckerGroupId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
chainId
timestamp
type
txHash
from
payEvent { amount, memo }
cashOutTokensEvent { cashOutCount, reclaimAmount }
}
}
}
List Active Loans
query ListLoans($projectId: Int!, $chainId: Int!, $limit: Int!) {
loans(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "createdAt"
orderDirection: "desc"
limit: $limit
) {
items {
id
borrowAmount
collateral
prepaidDuration
prepaidFeePercent
owner
beneficiary
createdAt
}
totalCount
}
}
Get Loan by ID
query GetLoan($id: BigInt!) {
loan(id: $id) {
id
projectId
chainId
borrowAmount
collateral
sourceFeeAmount
prepaidDuration
prepaidFeePercent
beneficiary
owner
token
terminal
tokenUri
createdAt
}
}
Get Wallet Portfolio
query GetWallet($address: String!) {
wallet(address: $address) {
address
volume
volumeUsd
lastPaidTimestamp
participants(limit: 100) {
items {
projectId
chainId
balance
volume
}
}
nfts(limit: 50) {
items {
projectId
chainId
tokenId
tierId
}
}
}
}
List NFT Tiers
query ListNFTTiers($projectId: Int!, $chainId: Int!, $limit: Int!) {
nftTiers(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "tierId"
orderDirection: "asc"
limit: $limit
) {
items {
tierId
price
initialSupply
remainingSupply
category
votingUnits
resolvedUri
metadata
svg
}
}
}
Get NFT Hook Details
query GetNFTHook($projectId: Int!, $chainId: Int!) {
nftHooks(
where: { projectId: $projectId, chainId: $chainId }
limit: 1
) {
items {
address
name
symbol
nftTiers(limit: 100) {
items {
tierId
price
remainingSupply
}
}
}
}
}
Get Historical Project Snapshots
query GetProjectHistory($projectId: Int!, $chainId: Int!, $limit: Int!) {
projectMoments(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
block
timestamp
volume
volumeUsd
balance
trendingScore
}
}
}
Get Cross-Chain Bridge Transactions
query GetSuckerTransactions($suckerGroupId: String!, $limit: Int!) {
suckerTransactions(
where: { suckerGroupId: $suckerGroupId }
orderBy: "createdAt"
orderDirection: "desc"
limit: $limit
) {
items {
index
chainId
peerChainId
beneficiary
projectTokenCount
terminalTokenAmount
status
createdAt
}
}
}
List Payout Events
query ListPayouts($projectId: Int!, $chainId: Int!, $limit: Int!) {
sendPayoutsEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
caller
rulesetCycleNumber
amount
amountPaidOut
fee
netLeftoverPayoutAmount
}
}
}
List Allowance Usage
query ListAllowanceUsage($projectId: Int!, $chainId: Int!, $limit: Int!) {
useAllowanceEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
caller
beneficiary
amount
netAmount
rulesetCycleNumber
}
}
}
List Loan Events (Borrow/Repay/Liquidate)
query ListBorrowEvents($projectId: Int!, $chainId: Int!, $limit: Int!) {
borrowLoanEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
borrowAmount
collateral
prepaidDuration
beneficiary
}
}
}
query ListRepayEvents($projectId: Int!, $chainId: Int!, $limit: Int!) {
repayLoanEvents(
where: { projectId: $projectId, chainId
items
timestamp
txHash
loanId
repayBorrowAmount
collateralCountToReturn
ListLiquidations: Int, : Int, : Int
liquidateLoanEvents
,
items
timestamp
txHash
borrowAmount
collateral
List Token Burns
query ListBurns($projectId: Int!, $chainId: Int!, $limit: Int!) {
burnEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
from
amount
creditAmount
erc20Amount
}
}
}
List NFT Mints
query ListNFTMints($projectId: Int!, $chainId: Int!, $limit: Int!) {
mintNftEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
beneficiary
tierId
tokenId
totalAmountPaid
}
}
}
Get Permission Holders
query ListPermissionHolders($projectId: Int!, $chainId: Int!) {
permissionHolders(
where: { projectId: $projectId, chainId: $chainId }
limit: 100
) {
items {
account
operator
permissions
isRevnetOperator
}
}
}
List Project Creations
query ListProjectCreations($chainId: Int!, $limit: Int!) {
projectCreateEvents(
where: { chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
projectId
caller
project {
name
handle
owner
}
}
}
}
List ERC20 Deployments
query ListERC20Deployments($chainId: Int!, $limit: Int!) {
deployErc20Events(
where: { chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
projectId
name
symbol
token
}
}
}
Get Cash Out Tax History
query GetCashOutTaxHistory($projectId: Int!, $chainId: Int!, $limit: Int!) {
cashOutTaxSnapshots(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "start"
orderDirection: "desc"
limit: $limit
) {
items {
start
duration
rulesetId
cashOutTax
}
}
}
Get Participant History (GraphQL Snapshots)
query GetParticipantHistory(
$projectId: Int!,
$chainId: Int!,
$address: String!,
$limit: Int!
) {
participantSnapshots(
where: { projectId: $projectId, chainId: $chainId, address: $address }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
block
timestamp
balance
creditBalance
erc20Balance
volume
volumeUsd
}
}
}
List Reserved Token Distributions
query ListReservedDistributions($projectId: Int!, $chainId: Int!, $limit: Int!) {
sendReservedTokensToSplitsEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
rulesetCycleNumber
tokenCount
leftoverAmount
owner
}
}
}
List Split Payouts
query ListSplitPayouts($projectId: Int!, $chainId: Int!, $limit: Int!) {
sendPayoutToSplitEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
beneficiary
splitProjectId
amount
netAmount
percent
}
}
}
Filtering
The where clause supports these operators:
where: {
projectId: 1
chainId: 1
chainId_in: [1, 10, 8453]
balance_gt: "1000000000000000000"
balance_gte: "1000000000000000000"
balance_lt: "100000000000000000000"
balance_lte: "100000000000000000000"
timestamp_gte: 1704067200
name_contains: "dao"
handle_starts_with: "jb"
}
Sorting
orderBy: "volume"
orderDirection: "desc"
Sortable Fields by Entity:
| Entity | Sortable Fields |
|---|
| Project | volume, balance, tokenSupply, paymentsCount, createdAt, trendingScore |
| Participant | balance, volume, paymentsCount |
| PayEvent | timestamp, amount |
| CashOutEvent | timestamp, reclaimAmount |
| NFT | createdAt, tokenId |
| ActivityEvent | timestamp |
| Loan | createdAt, borrowAmount, collateral |
| Wallet | volume, volumeUsd, lastPaidTimestamp |
| NFTTier | tierId, price, createdAt |
| ProjectMoment | timestamp, block |
| SuckerTransaction | createdAt |
| SendPayoutsEvent | timestamp, amount |
| UseAllowanceEvent | timestamp, amount |
| BorrowLoanEvent | timestamp, borrowAmount |
| RepayLoanEvent | timestamp, repayBorrowAmount |
| LiquidateLoanEvent | timestamp |
| BurnEvent | timestamp, amount |
| MintTokensEvent | timestamp, tokenCount |
| MintNftEvent |
Pagination
All list queries support pagination:
query PaginatedPayments(
$projectId: Int!,
$chainId: Int!,
$limit: Int!,
$offset: Int!
) {
payEvents(
where: { projectId: $projectId, chainId: $chainId }
limit: $limit
offset: $offset
) {
items { ... }
totalCount
}
}
Parameters:
limit: Max items to return (default: 100, max: 1000)
offset: Number of items to skip
Special Endpoints
Participant Snapshots
Get historical participant balances at a specific timestamp. Useful for governance snapshots and airdrops.
POST https://bendystraw.xyz/{API_KEY}/participants
Request:
{
"suckerGroupId": "0x...",
"timestamp": 1704067200
}
Response:
{
"participants": [
{
"address": "0x...",
"balance": "1000000000000000000000",
"chains": {
"1": "600000000000000000000",
"10": "400000000000000000000"
}
}
]
}
Complete JavaScript Example
const BENDYSTRAW_URL = 'https://bendystraw.xyz';
const API_KEY = process.env.BENDYSTRAW_API_KEY;
async function query(graphql, variables = {}) {
const response = await fetch(`${BENDYSTRAW_URL}/${API_KEY}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: graphql, variables })
});
const result = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
return result.data;
}
async function getProject(projectId, chainId) {
const data = await query(`
query($projectId: Int!, $chainId: Int!) {
project(projectId: $projectId, chainId: $chainId) {
name
handle
owner
balance
volume
volumeUsd
tokenSupply
paymentsCount
contributorsCount
suckerGroupId
}
}
`, { projectId, chainId });
data.;
}
() {
data = (, { : suckerGroupId });
data.;
}
() {
data = (, { projectId, chainId, limit });
data..;
}
() {
data = (, { projectId, chainId, limit });
{
: data..,
: data..
};
}
() {
data = (, { projectId, chainId, address });
data.;
}
() {
where = chainId ? { chainId } : {};
data = (, { where, limit });
data..;
}
() {
response = (, {
: ,
: { : },
: .({ suckerGroupId, timestamp })
});
response.();
}
() {
data = (, { projectId, chainId, limit });
data..;
}
() {
data = (, { projectId, chainId, limit });
{
: data..,
: data..
};
}
() {
data = (, { address });
data.;
}
() {
data = (, { projectId, chainId });
data..;
}
() {
data = (, { projectId, chainId, limit });
data..;
}
() {
project = (, );
.();
(project.) {
omni = (project.);
.();
}
payments = (, , );
.();
payments.( {
.();
});
{ holders, total } = (, , );
.();
holders.( {
.();
});
}
().(.);
BendystrawClient Class
class BendystrawClient {
constructor(apiKey, isTestnet = false) {
this.baseUrl = isTestnet
? 'https://testnet.bendystraw.xyz'
: 'https://bendystraw.xyz';
this.apiKey = apiKey;
}
async query(graphql, variables = {}) {
const response = await fetch(`${this.baseUrl}/${this.apiKey}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: graphql, variables })
});
if (!response.ok) {
throw new Error(`Bendystraw request failed: ${response.statusText}`);
}
const result = await response.json();
if (result.errors) {
throw new Error(result.errors[0].message);
}
return result.;
}
() { ... }
() { ... }
() { ... }
() { ... }
() { ... }
}
Server-Side Proxy
Since the API key must be kept secret, use a server-side proxy:
Next.js API Route
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const response = await fetch(
`https://bendystraw.xyz/${process.env.BENDYSTRAW_API_KEY}/graphql`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
}
);
const data = await response.json();
res.json(data);
}
Express Middleware
app.post('/api/bendystraw', async (req, res) => {
const response = await fetch(
`https://bendystraw.xyz/${process.env.BENDYSTRAW_API_KEY}/graphql`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req.body)
}
);
res.json(await response.json());
});
Error Handling
| Error | Cause | Solution |
|---|
Unauthorized | Invalid API key | Verify API key is correct |
Rate limited | Too many requests | Add backoff/retry logic |
Invalid query | GraphQL syntax error | Check query structure |
Not found | Entity doesn't exist | Verify projectId/chainId |
Timeout | Query too complex | Reduce limit, add filters |
Gotchas & Common Pitfalls
V4 vs V5 Protocol Confusion
CRITICAL: V4 and V5 are completely different protocols with different contract addresses.
Project #1 on V4 is NOT the same as Project #1 on V5. Never mix V4 and V5 addresses.
V5.0 vs V5.1 Contract Confusion
CRITICAL: Within V5, there are two contract versions: V5.0 and V5.1. Contracts that have
both versions MUST NOT be mixed. A project using JBController5_1 MUST use JBMultiTerminal5_1.
Shared contracts (work with both V5.0 and V5.1):
| Contract | Address |
|---|
| JBProjects | 0x885f707efa18d2cb12f05a3a8eba6b4b26c8c1d4 |
| JBTokens | 0x4d0edd347fb1fa21589c1e109b3474924be87636 |
| JBDirectory | 0x0061e516886a0540f63157f112c0588ee0651dcf |
| JBSplits | 0x7160a322fea44945a6ef9adfd65c322258df3c5e |
V5.0 contracts (for revnets and older projects):
| Contract | Address |
|---|
| JBController | 0x27da30646502e2f642be5281322ae8c394f7668a |
| JBMultiTerminal | 0x2db6d704058e552defe415753465df8df0361846 |
| JBRulesets | 0x6292281d69c3593fcf6ea074e5797341476ab428 |
| REVDeployer | 0x2ca27bde7e7d33e353b44c27acfcf6c78dde251d |
| JB721TiersHookDeployer | 0x7e4f7bfeab74bbae3eb12a62f2298bf2be16fc93 |
V5.1 contracts (for new projects):
| Contract | Address |
|---|
| JBController5_1 | 0xf3cc99b11bd73a2e3b8815fb85fe0381b29987e1 |
| JBMultiTerminal5_1 | 0x52869db3d61dde1e391967f2ce5039ad0ecd371c |
| JBRulesets5_1 | 0xd4257005ca8d27bbe11f356453b0e4692414b056 |
| JBOmnichainDeployer5_1 | 0x587bf86677ec0d1b766d9ba0d7ac2a51c6c2fc71 |
| JB721TiersHookDeployer5_1 | 0x7e6e7db5081c59f2df3c83b54eb0c4d029e9898e |
Determining project version: Query JBDirectory.controllerOf(projectId) and compare:
0x27da30646502e2f642be5281322ae8c394f7668a = V5.0 (use JBMultiTerminal, JBRulesets)
0xf3cc99b11bd73a2e3b8815fb85fe0381b29987e1 = V5.1 (use JBMultiTerminal5_1, JBRulesets5_1)
See /jb-v5-v51-contracts for complete reference and code patterns.
Token Symbol Confusion
CRITICAL: The tokenSymbol field in Bendystraw returns the base/accounting token (e.g., "ETH" or "USDC"), NOT the project's issued ERC20 token symbol (e.g., "NANA" for Bananapus).
To get the project's issued token symbol, you must query the blockchain directly:
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const JB_TOKENS = '0x4d0edd347fb1fa21589c1e109b3474924be87636'
const TOKEN_ABI = [
{
name: 'tokenOf',
type: 'function',
stateMutability: 'view',
inputs: [{ name: 'projectId', type: 'uint256' }],
outputs: [{ name: '', type: 'address' }],
},
{
name: 'symbol',
type: 'function',
stateMutability: 'view',
inputs: [],
outputs: [{ name: '', type: 'string' }],
},
]
async function getProjectTokenSymbol(projectId: number, chainId: number) {
const client = createPublicClient({ chain: mainnet, transport: http() })
const tokenAddress = client.({
: ,
: ,
: ,
: [(projectId)],
})
(tokenAddress === ) {
}
client.({
: tokenAddress,
: ,
: ,
})
}
GraphQL Type Inconsistencies
Different queries expect different GraphQL types for the same conceptual values. This causes silent failures if you use the wrong type:
| Query | projectId type | chainId type | version type |
|---|
project() | Float! | Float! | Float! |
payEvents() | Int! | Int! | Int! |
participants() | Int! | Int | - |
projects() | - | - | - |
Example of the issue:
query GetProject($projectId: Float!, $chainId: Float!, $version: Float!) {
project(projectId: $projectId, chainId: $chainId, version: $version) { ... }
}
query GetPayEvents($projectId: Int!, $chainId: Int!, $version: Int!) {
payEvents(where: { projectId: $projectId, chainId: $chainId, version: $version }) { ... }
}
Best practice: Check the schema or use the GraphQL playground to verify expected types for each query.
SuckerGroup Cross-Chain Aggregation
For omnichain projects, use suckerGroupId to get aggregated data across all chains instead of querying each chain separately:
const chains = [1, 10, 8453, 42161]
const balances = await Promise.all(
chains.map(chainId => getProjectBalance(projectId, chainId))
)
const totalBalance = balances.reduce((sum, b) => sum + b, 0n)
const project = await getProject(projectId, chainId)
if (project.suckerGroupId) {
const group = await getSuckerGroup(project.suckerGroupId)
}
The suckerGroup query returns:
- Pre-aggregated totals (
balance, volume, tokenSupply, etc.)
- Per-chain breakdown via
projects_rel
- Consistent data without race conditions from parallel queries
ETH vs USDC Project Currency
CRITICAL: Projects can use different base currencies (ETH or USDC). The amount, balance, and volume fields use different decimal precision depending on the currency:
| Currency | Decimals | Code |
|---|
| ETH | 18 | 1 |
| USDC | 6 | 2 |
The currency info is available from the suckerGroup or participants query:
query GetSuckerGroup($id: String!) {
suckerGroup(id: $id) {
projects_rel {
projectId
chainId
decimals
currency
balance
}
}
}
Formatting amounts correctly:
import { formatUnits } from 'viem'
function formatAmount(wei, decimals, currency) {
const num = parseFloat(formatUnits(BigInt(wei), decimals))
const symbol = currency === 2 ? 'USDC' : 'ETH'
const precision = currency === 2 ? 2 : 4
return `${num.toFixed(precision)} ${symbol}`
}
formatAmount('1000000', 6, 2)
formatAmount('1000000000000000000', 18, 1)
Common mistake: Using formatEther() (assumes 18 decimals) for USDC projects will show wildly incorrect values. Always detect the currency first and use formatUnits() with the correct decimals.
Best Practices
- Use server-side proxy - Never expose API key in frontend code
- Cache responses - Data updates every ~1 minute, cache accordingly
- Query only needed fields - Reduces payload size and latency
- Use pagination - Don't fetch thousands of records at once
- Handle nulls - Fields like
volumeUsd, handle may be null
- Consider freshness - Indexer may lag 1-2 blocks behind chain
- Use filters - Narrow queries by chainId, projectId when possible
- Always include version - V4 and V5 projects with the same projectId are completely different
- Use BigInt for USD values - volumeUsd is 18 decimals; parseFloat loses precision on large values
- Group by projectId + version - Same project across chains should be grouped, but different versions should not
- Check GraphQL types - Different queries expect Float vs Int for the same fields
- Use suckerGroup for cross-chain data - More efficient than querying each chain separately
- Fetch token symbols from chain - Bendystraw's tokenSymbol is the accounting token, not the project's issued token
- Detect currency before formatting - Use
formatUnits(wei, decimals) not formatEther(wei) since USDC projects use 6 decimals
Use Cases
- Project dashboards - Display stats, activity, holders
- Unified activity feeds - Use
activityEvents for all-in-one activity streams
- Governance snapshots - Get token balances at specific timestamps
- Analytics - Track trends, volumes, contributor growth via
projectMoments
- Portfolio tracking - Use
wallet query for user positions across all projects
- Omnichain aggregation - Unified view via
suckerGroup and suckerGroupMoments
- Airdrops - Generate recipient lists from holder data
- Loan dashboards - Track RevLoans borrowing, repayments, liquidations
- NFT galleries - Full tier metadata via
nftTiers including SVGs and pricing
- Cross-chain tracking - Monitor token bridging via
suckerTransactions
- Treasury management - Track payouts and allowance usage
Related Skills
/jb-relayr - Execute multi-chain transactions
/jb-omnichain-ui - Build UIs with Bendystraw data
/jb-query - Direct on-chain queries via cast/ethers
/jb-revloans - RevLoans protocol integration
/jb-loan-queries - Loan-specific query patterns
/jb-nft-gallery-ui - Build NFT galleries with tier data