技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
jb-bendystraw【数据与研究】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.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
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.
Production: https://bendystraw.xyz/{API_KEY}/graphql
Testnet: https://testnet.bendystraw.xyz/{API_KEY}/graphql
Playground: https://bendystraw.xyz (browser-based GraphQL explorer)
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.
| Chain | Chain ID | Network |
|---|---|---|
| Ethereum | 1 | Mainnet |
| Optimism | 10 | Mainnet |
| Base | 8453 | Mainnet |
| Arbitrum | 42161 | Mainnet |
| Sepolia | 11155111 | Testnet |
type Project {
# Identifiers
id: String! # "{chainId}-{projectId}-{version}"
projectId: Int!
chainId: Int!
version: Int! # Protocol version (4 or 5)
# Metadata
handle: String
name: String
description: String
logoUri: String
infoUri: String
owner: String!
deployer: String
# Financial
balance: String! # Current balance (wei)
volume: String! # Total received (wei)
volumeUsd: String # USD equivalent
redeemVolume: String! # Total redeemed (wei)
redeemVolumeUsd: String
# Tokens
tokenSupply: String! # Total token supply
token: String # ERC20 address if deployed
tokenSymbol: String
# Activity counts
paymentsCount: Int!
redeemCount: Int!
contributorsCount: Int!
nftsMintedCount: Int!
# Trending (7-day window)
trendingScore: Float
trendingVolume: String
trendingPaymentsCount: Int
# Omnichain
suckerGroupId: String # Linked cross-chain group
# Timestamps
createdAt: Int!
deployedAt: Int
}
type SuckerGroup {
id: String! # Unique group identifier
projects: [String!]! # Array of project IDs
# Aggregated totals across all chains
volume: String!
volumeUsd: String
balance: String!
tokenSupply: String!
paymentsCount: Int!
contributorsCount: Int!
# Related projects (expanded)
projects_rel: [Project!]!
}
type Participant {
id: String! # "{chainId}-{projectId}-{address}"
address: String!
projectId: Int!
chainId: Int!
# Balances
balance: String! # Total balance (credits + ERC20)
creditBalance: String! # Unclaimed credits
erc20Balance: String! # Claimed ERC20 tokens
# Activity
volume: String! # Total contributed
volumeUsd: String
paymentsCount: Int!
redeemCount: Int!
# Timestamps
lastPaidAt: Int
firstPaidAt: Int
}
type PayEvent {
id: String!
projectId: Int!
chainId: Int!
rulesetId: Int!
# Transaction
txHash: String!
timestamp: Int!
logIndex: Int!
blockNumber: Int!
# Payment details
from: String! # Payer address
beneficiary: String! # Token recipient
amount: String! # Payment amount (wei)
amountUsd: String
distributionFromPayAmount: String!
# Tokens
newlyIssuedTokenCount: String!
beneficiaryTokenCount: String!
# Metadata
memo: String
feeFromPayAmount: String
}
type CashOutEvent {
id: String!
projectId: Int!
chainId: Int!
rulesetId: Int!
# Transaction
txHash: String!
timestamp: Int!
# Redemption details
holder: String!
beneficiary: String!
cashOutCount: String! # Tokens burned
reclaimAmount: String! # ETH received
reclaimAmountUsd: String
metadata: String
}
type NFT {
id: String!
tokenId: Int!
projectId: Int!
chainId: Int!
hook: String! # 721 hook address
# Tier
tierId: Int!
tierCategory: Int
# Ownership
owner: String!
createdAt: Int!
# Metadata
tokenUri: String
}
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!
# Transaction
txHash: String!
timestamp: Int!
from: String!
# Event type discriminator
type: ActivityEventType! # Determines which embedded event is populated
# Embedded events (one will be non-null based on type)
payEvent: PayEvent
cashOutTokensEvent: CashOutTokensEvent
mintNftEvent: MintNftEvent
sendPayoutsEvent: SendPayoutsEvent
sendPayoutToSplitEvent: SendPayoutToSplitEvent
borrowLoanEvent: BorrowLoanEvent
repayLoanEvent: RepayLoanEvent
liquidateLoanEvent: LiquidateLoanEvent
deployErc20Event: DeployErc20Event
burnEvent: BurnEvent
mintTokensEvent: MintTokensEvent
projectCreateEvent: ProjectCreateEvent
addToBalanceEvent: AddToBalanceEvent
useAllowanceEvent: UseAllowanceEvent
decorateBannyEvent: DecorateBannyEvent
# ... and more
# Relations
project: Project
suckerGroup: SuckerGroup
}
enum ActivityEventType {
payEvent
cashOutTokensEvent
mintNftEvent
sendPayoutsEvent
sendPayoutToSplitEvent
borrowLoanEvent
repayLoanEvent
liquidateLoanEvent
reallocateLoanEvent
deployErc20Event
burnEvent
mintTokensEvent
manualMintTokensEvent
manualBurnEvent
autoIssueEvent
projectCreateEvent
addToBalanceEvent
useAllowanceEvent
sendReservedTokensToSplitEvent
sendReservedTokensToSplitsEvent
decorateBannyEvent
}
Active loan state from the RevLoans protocol.
type Loan {
id: BigInt! # Loan ID (NFT token ID)
projectId: Int!
chainId: Int!
version: Int!
createdAt: Int!
# Loan terms
borrowAmount: BigInt! # Amount borrowed (wei)
collateral: BigInt! # Collateral locked (project tokens)
sourceFeeAmount: BigInt! # Fee amount
prepaidDuration: Int! # Prepaid period in seconds
prepaidFeePercent: Int! # Fee percent (basis points)
# Addresses
beneficiary: String! # Who receives borrowed funds
owner: String! # Loan NFT owner
token: String! # Collateral token address
terminal: String! # Terminal address
# Metadata
tokenUri: String # Loan NFT metadata URI
# Relations
project: Project
participant: Participant
wallet: Wallet
}
Wallet-level aggregation across all project participations.
type Wallet {
address: String! # Wallet address
# Aggregated stats
volume: BigInt! # Total volume across all projects
volumeUsd: BigInt! # USD equivalent (18 decimals)
lastPaidTimestamp: Int # Most recent payment timestamp
# Relations
participants: ParticipantPage # All project participations
nfts: NFTPage # All owned NFTs
}
NFT tier configuration with pricing and supply.
type NFTTier {
chainId: Int!
projectId: Int!
version: Int!
tierId: Int!
# Pricing
price: BigInt! # Price in terminal token (wei)
# Supply
initialSupply: Int! # Original supply
remainingSupply: Int! # Current available
# Configuration
allowOwnerMint: Boolean! # Owner can mint without payment
cannotBeRemoved: Boolean! # Tier is permanent
transfersPausable: Boolean! # Transfers can be paused
votingUnits: BigInt! # Governance weight per NFT
category: Int # Tier category
reserveFrequency: Int # Reserve rate
reserveBeneficiary: String # Reserve recipient
# Metadata
encodedIpfsUri: String # IPFS hash (encoded)
resolvedUri: String # Full resolved URI
metadata: JSON # Parsed metadata
svg: String # SVG content if available
createdAt: Int!
# Relations
hook: NFTHook
nfts: NFTPage # Minted NFTs in this tier
project: Project
}
721 hook contract configuration.
type NFTHook {
chainId: Int!
projectId: Int!
version: Int!
createdAt: Int!
address: String! # Hook contract address
name: String # Collection name
symbol: String # Collection symbol
# Relations
nfts: NFTPage
nftTiers: NFTTierPage
project: Project
}
Point-in-time snapshots of project state. Useful for historical charts and analytics.
type ProjectMoment {
projectId: Int!
chainId: Int!
version: Int!
# Snapshot point
block: Int! # Block number
timestamp: Int! # Unix timestamp
# State at snapshot
volume: BigInt!
volumeUsd: BigInt!
balance: BigInt!
trendingScore: BigInt!
# Relations
project: Project
}
Point-in-time snapshots of cross-chain aggregated state.
type SuckerGroupMoment {
suckerGroupId: String!
# Snapshot point
block: Int!
timestamp: Int!
# Aggregated state
volume: BigInt!
volumeUsd: BigInt!
balance: BigInt!
tokenSupply: BigInt!
# Relations
suckerGroup: SuckerGroup
}
Token bridging transactions between chains via suckers.
type SuckerTransaction {
index: Int! # Transaction index
projectId: Int!
chainId: Int! # Source chain
version: Int!
suckerGroupId: String!
createdAt: Int!
# Bridge details
token: String! # Token being bridged
sucker: String! # Source sucker address
peer: String! # Destination sucker address
peerChainId: Int! # Destination chain
beneficiary: String! # Recipient address
# Amounts
projectTokenCount: BigInt! # Project tokens bridged
terminalTokenAmount: BigInt! # Terminal tokens (if any)
# State
root: String # Merkle root
status: SuckerTransactionStatus!
# Relations
suckerGroup: SuckerGroup
}
enum SuckerTransactionStatus {
pending
completed
failed
}
Payout distribution events from the terminal.
type SendPayoutsEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
logIndex: Int!
# Payout details
caller: String! # Who triggered payout
from: String! # Source address
rulesetId: Int!
rulesetCycleNumber: Int!
# Amounts
amount: BigInt! # Total payout amount
amountUsd: BigInt!
amountPaidOut: BigInt! # Actually distributed
amountPaidOutUsd: BigInt!
netLeftoverPayoutAmount: BigInt! # Remaining after splits
fee: BigInt! # Protocol fee
feeUsd: BigInt!
# Relations
project: Project
}
Surplus allowance usage events.
type UseAllowanceEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
# Transaction
txHash: String!
timestamp: Int!
# Allowance details
caller: String!
beneficiary: String! # Who receives funds
rulesetId: Int!
rulesetCycleNumber: Int!
# Amounts
amount: BigInt! # Amount used
amountUsd: BigInt!
netAmount: BigInt! # After fees
netAmountUsd: BigInt!
# Relations
project: Project
}
Operator permissions granted to accounts.
type PermissionHolder {
chainId: Int!
projectId: Int!
version: Int!
account: String! # Account with permissions
operator: String! # Operator address
permissions: [Int!]! # Permission IDs granted
isRevnetOperator: Boolean! # Is a revnet operator
# Relations
project: Project
}
Loan creation events from RevLoans.
type BorrowLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Loan details
borrowAmount: BigInt! # Amount borrowed
collateral: BigInt! # Collateral locked
sourceFeeAmount: BigInt! # Fee paid
prepaidDuration: Int! # Prepaid period (seconds)
prepaidFeePercent: Int! # Fee percent (basis points)
beneficiary: String! # Loan recipient
token: String! # Collateral token
terminal: String! # Terminal address
# Relations
project: Project
}
Loan repayment events.
type RepayLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Repayment details
loanId: BigInt! # Loan being repaid
paidOffLoanId: BigInt # If fully paid off
repayBorrowAmount: BigInt! # Amount repaid
collateralCountToReturn: BigInt! # Collateral returned
# Relations
project: Project
}
Loan liquidation events.
type LiquidateLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Liquidation details
borrowAmount: BigInt! # Outstanding borrow
collateral: BigInt! # Collateral seized
# Relations
project: Project
}
Loan reallocation events (moving collateral between loans).
type ReallocateLoanEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Reallocation details
loanId: BigInt! # Source loan
reallocatedLoanId: BigInt! # Target loan
removedCollateralCount: BigInt! # Collateral moved
# Relations
project: Project
}
Token burn events (from cash outs).
type BurnEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
from: String!
# Burn details
amount: BigInt! # Total burned
creditAmount: BigInt! # Credits burned
erc20Amount: BigInt! # ERC20 burned
# Relations
project: Project
}
Token minting events (from payments).
type MintTokensEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Mint details
beneficiary: String! # Token recipient
beneficiaryTokenCount: BigInt! # Tokens to beneficiary
reservedPercent: BigInt! # Reserved rate
tokenCount: BigInt! # Total minted
memo: String # Payment memo
# Relations
project: Project
}
Manual token minting by project owner.
type ManualMintTokensEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Mint details
beneficiary: String!
beneficiaryTokenCount: BigInt!
reservedPercent: BigInt!
tokenCount: BigInt!
memo: String
# Relations
project: Project
}
Manual token burning.
type ManualBurnEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
from: String!
# Burn details
amount: BigInt!
creditAmount: BigInt!
erc20Amount: BigInt!
# Relations
project: Project
}
NFT minting events.
type MintNftEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Mint details
hook: String! # 721 hook address
beneficiary: String! # NFT recipient
tierId: Int! # Tier minted
tokenId: BigInt! # Token ID
totalAmountPaid: BigInt! # Amount paid
# Relations
project: Project
tier: NFTTier
nft: NFT
}
ERC20 token deployment events.
type DeployErc20Event {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Token details
symbol: String! # Token symbol
name: String! # Token name
token: String! # Token address
# Relations
project: Project
}
Project creation events.
type ProjectCreateEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Relations
project: Project
}
Direct balance addition events.
type AddToBalanceEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Details
amount: BigInt! # Amount added
memo: String # Memo
metadata: String # Additional metadata
returnedFees: BigInt # Fees returned
# Relations
project: Project
}
Individual split payout events.
type SendPayoutToSplitEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Split details
rulesetId: Int!
group: BigInt! # Split group
beneficiary: String! # Split recipient
splitProjectId: Int # If split to project
hook: String # Split hook if any
# Amounts
amount: BigInt! # Gross amount
netAmount: BigInt! # After fees
amountUsd: BigInt!
percent: Int! # Split percent
lockedUntil: BigInt # Lock timestamp
preferAddToBalance: Boolean! # Add to balance vs pay
# Relations
project: Project
}
Reserved token distribution to individual split.
type SendReservedTokensToSplitEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Split details
rulesetId: Int!
groupId: BigInt!
beneficiary: String!
splitProjectId: Int
hook: String
tokenCount: BigInt! # Tokens sent
percent: Int!
lockedUntil: BigInt
preferAddToBalance: Boolean!
# Relations
project: Project
}
Batch reserved token distribution event.
type SendReservedTokensToSplitsEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
suckerGroupId: String
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Distribution details
rulesetId: Int!
rulesetCycleNumber: Int!
owner: String! # Project owner
tokenCount: BigInt! # Total distributed
leftoverAmount: BigInt! # Remaining after splits
# Relations
project: Project
}
Auto-issuance events (revnet stage transitions).
type AutoIssueEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Issuance details
stageId: BigInt! # Revnet stage
beneficiary: String! # Token recipient
count: BigInt! # Tokens issued
# Relations
project: Project
}
Auto-issuance configuration events.
type StoreAutoIssuanceAmountEvent {
id: String!
chainId: Int!
version: Int!
projectId: Int!
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Config details
stageId: BigInt! # Revnet stage
beneficiary: String! # Configured recipient
count: BigInt! # Amount to auto-issue
# Relations
project: Project
}
Banny NFT decoration events.
type DecorateBannyEvent {
id: String!
chainId: Int!
version: Int!
# Transaction
txHash: String!
timestamp: Int!
caller: String!
from: String!
logIndex: Int!
# Decoration details
bannyBodyId: BigInt! # Banny being decorated
outfitIds: [BigInt!]! # Outfit NFT IDs
backgroundId: BigInt # Background NFT ID
tokenUri: String # Updated token URI
tokenUriMetadata: JSON # Parsed metadata
# Relations
bannyNft: NFT
}
Historical cash-out tax rate snapshots.
type CashOutTaxSnapshot {
chainId: Int!
projectId: Int!
suckerGroupId: String
version: Int!
# Snapshot period
start: BigInt! # Period start timestamp
duration: BigInt! # Period duration
rulesetId: BigInt! # Ruleset ID
# Tax rate
cashOutTax: Int! # Tax rate (basis points)
}
Historical participant balance snapshots via GraphQL (alternative to REST endpoint).
type ParticipantSnapshot {
chainId: Int!
projectId: Int!
suckerGroupId: String
version: Int!
# Snapshot point
block: Int!
timestamp: Int!
# Participant
address: String!
# Balances at snapshot
balance: BigInt!
creditBalance: BigInt!
erc20Balance: BigInt!
volume: BigInt!
volumeUsd: BigInt!
}
A Juicebox project is uniquely identified by three fields: projectId + chainId + version.
This is crucial because:
version when querying or displaying projects.// WRONG: Groups V4 and V5 together
const groupKey = `${project.projectId}-${project.chainId}`;
// CORRECT: Keeps V4 and V5 separate
const groupKey = `${project.projectId}-v${project.version}`;
When displaying "top projects" or aggregating stats:
// Group projects by projectId + version (V4 and V5 are 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) {
// Add chain to existing group
if (!existing.chainIds.includes(project.chainId)) {
existing.chainIds.push(project.chainId);
}
// Sum volumes
existing.totalVolumeUsd += parseFloat(project.volumeUsd || '0');
} else {
grouped.set(groupKey, {
...project,
chainIds: [project.chainId],
totalVolumeUsd: parseFloat(project.volumeUsd || '0')
});
}
}
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 {
// volumeUsd comes in 18 decimal format
// Use BigInt to avoid precision loss on large numbers
const raw = BigInt(volumeUsd.split('.')[0]);
const usd = Number(raw / BigInt(1e12)) / 1e6; // Divide in steps
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.
When querying projects, filter by version to avoid mixing V4 and V5 data:
# Get only V5 projects
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 GetProject($projectId: Int!, $chainId: Int!) {
project(projectId: $projectId, chainId: $chainId) {
id
name
handle
owner
balance
volume
volumeUsd
tokenSupply
paymentsCount
contributorsCount
suckerGroupId
}
}
query GetParticipant($projectId: Int!, $chainId: Int!, $address: String!) {
participant(projectId: $projectId, chainId: $chainId, address: $address) {
balance
creditBalance
erc20Balance
volume
volumeUsd
paymentsCount
}
}
query GetSuckerGroup($id: String!) {
suckerGroup(id: $id) {
id
volume
volumeUsd
balance
tokenSupply
paymentsCount
contributorsCount
projects_rel {
projectId
chainId
name
balance
volume
}
}
}
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
}
}
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
}
}
}
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
}
}
query TrendingProjects($limit: Int!) {
projects(
orderBy: "trendingScore"
orderDirection: "desc"
limit: $limit
) {
items {
projectId
chainId
name
handle
trendingScore
trendingVolume
trendingPaymentsCount
}
}
}
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
}
}
}
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
}
}
}
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 # Discriminator for event type
# Include fields from each possible event type
payEvent {
amount
amountUsd
beneficiary
memo
}
cashOutTokensEvent {
cashOutCount
reclaimAmount
holder
}
mintNftEvent {
tierId
tokenId
}
sendPayoutsEvent {
amount
amountPaidOut
fee
}
borrowLoanEvent {
borrowAmount
collateral
}
}
}
}
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 }
}
}
}
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
}
}
query GetLoan($id: BigInt!) {
loan(id: $id) {
id
projectId
chainId
borrowAmount
collateral
sourceFeeAmount
prepaidDuration
prepaidFeePercent
beneficiary
owner
token
terminal
tokenUri
createdAt
}
}
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
}
}
}
}
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
}
}
}
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
}
}
}
}
}
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
}
}
}
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
}
}
}
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
}
}
}
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
}
}
}
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: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
loanId
repayBorrowAmount
collateralCountToReturn
}
}
}
query ListLiquidations($projectId: Int!, $chainId: Int!, $limit: Int!) {
liquidateLoanEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
borrowAmount
collateral
}
}
}
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
}
}
}
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
}
}
}
query ListPermissionHolders($projectId: Int!, $chainId: Int!) {
permissionHolders(
where: { projectId: $projectId, chainId: $chainId }
limit: 100
) {
items {
account
operator
permissions
isRevnetOperator
}
}
}
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
}
}
}
}
query ListERC20Deployments($chainId: Int!, $limit: Int!) {
deployErc20Events(
where: { chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
txHash
projectId
name
symbol
token
}
}
}
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
}
}
}
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
}
}
}
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
}
}
}
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
}
}
}
The where clause supports these operators:
where: {
# Exact match
projectId: 1
chainId: 1
# Multiple values (OR)
chainId_in: [1, 10, 8453]
# Comparison operators
balance_gt: "1000000000000000000"
balance_gte: "1000000000000000000"
balance_lt: "100000000000000000000"
balance_lte: "100000000000000000000"
timestamp_gte: 1704067200
# Text search
name_contains: "dao"
handle_starts_with: "jb"
}
orderBy: "volume" # Field to sort by
orderDirection: "desc" # "asc" or "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 | timestamp, tokenId |
| DeployErc20Event | timestamp |
| ProjectCreateEvent | timestamp |
| SendPayoutToSplitEvent | timestamp, amount |
| CashOutTaxSnapshot | start |
| ParticipantSnapshot | timestamp, block |
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 skipGet 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"
}
}
]
}
const BENDYSTRAW_URL = 'https://bendystraw.xyz';
const API_KEY = process.env.BENDYSTRAW_API_KEY;
/**
* Execute GraphQL query
*/
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;
}
/**
* Get project stats
*/
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 });
return data.project;
}
/**
* Get omnichain totals for sucker group
*/
async function getOmnichainStats(suckerGroupId) {
const data = await query(`
query($id: String!) {
suckerGroup(id: $id) {
volume
volumeUsd
balance
tokenSupply
contributorsCount
projects_rel {
chainId
name
balance
volume
}
}
}
`, { id: suckerGroupId });
return data.suckerGroup;
}
/**
* Get recent payments
*/
async function getRecentPayments(projectId, chainId, limit = 20) {
const data = await query(`
query($projectId: Int!, $chainId: Int!, $limit: Int!) {
payEvents(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "timestamp"
orderDirection: "desc"
limit: $limit
) {
items {
timestamp
from
beneficiary
amount
amountUsd
memo
newlyIssuedTokenCount
}
}
}
`, { projectId, chainId, limit });
return data.payEvents.items;
}
/**
* Get top token holders
*/
async function getTopHolders(projectId, chainId, limit = 100) {
const data = await query(`
query($projectId: Int!, $chainId: Int!, $limit: Int!) {
participants(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "balance"
orderDirection: "desc"
limit: $limit
) {
items {
address
balance
creditBalance
erc20Balance
volume
}
totalCount
}
}
`, { projectId, chainId, limit });
return {
holders: data.participants.items,
total: data.participants.totalCount
};
}
/**
* Get participant balance
*/
async function getParticipant(projectId, chainId, address) {
const data = await query(`
query($projectId: Int!, $chainId: Int!, $address: String!) {
participant(projectId: $projectId, chainId: $chainId, address: $address) {
balance
creditBalance
erc20Balance
volume
volumeUsd
paymentsCount
}
}
`, { projectId, chainId, address });
return data.participant;
}
/**
* Get trending projects
*/
async function getTrendingProjects(chainId = null, limit = 10) {
const where = chainId ? { chainId } : {};
const data = await query(`
query($where: ProjectWhereInput, $limit: Int!) {
projects(
where: $where
orderBy: "trendingScore"
orderDirection: "desc"
limit: $limit
) {
items {
projectId
chainId
name
handle
trendingScore
trendingVolume
trendingPaymentsCount
}
}
}
`, { where, limit });
return data.projects.items;
}
/**
* Get historical snapshot for governance
*/
async function getSnapshot(suckerGroupId, timestamp) {
const response = await fetch(`${BENDYSTRAW_URL}/${API_KEY}/participants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ suckerGroupId, timestamp })
});
return await response.json();
}
/**
* Get unified activity feed (all event types)
*/
async function getActivityFeed(projectId, chainId, limit = 50) {
const data = await query(`
query($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 }
}
}
}
`, { projectId, chainId, limit });
return data.activityEvents.items;
}
/**
* Get active loans for a project
*/
async function getLoans(projectId, chainId, limit = 100) {
const data = await query(`
query($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
}
}
`, { projectId, chainId, limit });
return {
loans: data.loans.items,
total: data.loans.totalCount
};
}
/**
* Get wallet portfolio across all projects
*/
async function getWalletPortfolio(address) {
const data = await query(`
query($address: String!) {
wallet(address: $address) {
address
volume
volumeUsd
lastPaidTimestamp
participants(limit: 100) {
items {
projectId
chainId
balance
volume
project { name, handle }
}
}
}
}
`, { address });
return data.wallet;
}
/**
* Get NFT tier details with metadata
*/
async function getNFTTiers(projectId, chainId) {
const data = await query(`
query($projectId: Int!, $chainId: Int!) {
nftTiers(
where: { projectId: $projectId, chainId: $chainId }
orderBy: "tierId"
orderDirection: "asc"
limit: 100
) {
items {
tierId
price
initialSupply
remainingSupply
category
votingUnits
resolvedUri
metadata
svg
}
}
}
`, { projectId, chainId });
return data.nftTiers.items;
}
/**
* Get historical project snapshots for charting
*/
async function getProjectHistory(projectId, chainId, limit = 100) {
const data = await query(`
query($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
}
}
}
`, { projectId, chainId, limit });
return data.projectMoments.items;
}
// Example usage
async function main() {
// Get project on Ethereum mainnet
const project = await getProject(1, 1);
console.log(`${project.name}: ${project.balance} wei balance`);
// If omnichain, get aggregated stats
if (project.suckerGroupId) {
const omni = await getOmnichainStats(project.suckerGroupId);
console.log(`Omnichain total: ${omni.volume} wei volume across ${omni.projects_rel.length} chains`);
}
// Get recent activity
const payments = await getRecentPayments(1, 1, 5);
console.log(`Last ${payments.length} payments:`);
payments.forEach(p => {
console.log(` ${p.from.slice(0,8)}... paid ${p.amount} wei`);
});
// Get top holders
const { holders, total } = await getTopHolders(1, 1, 10);
console.log(`Top 10 of ${total} holders:`);
holders.forEach((h, i) => {
console.log(` ${i + 1}. ${h.address.slice(0,8)}... - ${h.balance} tokens`);
});
}
main().catch(console.error);
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.data;
}
// Convenience methods
async getProject(projectId, chainId) { ... }
async getSuckerGroup(id) { ... }
async getPayments(projectId, chainId, limit) { ... }
async getParticipants(projectId, chainId, limit) { ... }
async getSnapshot(suckerGroupId, timestamp) { ... }
}
Since the API key must be kept secret, use a server-side proxy:
// pages/api/bendystraw.ts
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);
}
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 | 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 |
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.
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.
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'
// JBTokens address (same on all V5 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() })
// Get the token address from JBTokens
const tokenAddress = await client.readContract({
address: JB_TOKENS,
abi: TOKEN_ABI,
functionName: 'tokenOf',
args: [BigInt(projectId)],
})
// Zero address means no ERC20 deployed yet (using credits only)
if (tokenAddress === '0x0000000000000000000000000000000000000000') {
return null
}
// Get symbol from the token contract
return await client.readContract({
address: tokenAddress,
abi: TOKEN_ABI,
functionName: 'symbol',
})
}
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:
# This works (project query uses Float)
query GetProject($projectId: Float!, $chainId: Float!, $version: Float!) {
project(projectId: $projectId, chainId: $chainId, version: $version) { ... }
}
# This fails silently if you pass Float instead of Int
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.
For omnichain projects, use suckerGroupId to get aggregated data across all chains instead of querying each chain separately:
// INEFFICIENT: Query 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)
// EFFICIENT: Use suckerGroup for cross-chain totals
const project = await getProject(projectId, chainId)
if (project.suckerGroupId) {
const group = await getSuckerGroup(project.suckerGroupId)
// group.balance is already the cross-chain total
// group.projects_rel has per-chain breakdown
}
The suckerGroup query returns:
balance, volume, tokenSupply, etc.)projects_relCRITICAL: 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 # 18 for ETH, 6 for USDC
currency # 1 for ETH, 2 for USDC
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'
// USDC uses fewer decimal places for display
const precision = currency === 2 ? 2 : 4
return `${num.toFixed(precision)} ${symbol}`
}
// Example: USDC project (6 decimals)
formatAmount('1000000', 6, 2) // "1.00 USDC"
// Example: ETH project (18 decimals)
formatAmount('1000000000000000000', 18, 1) // "1.0000 ETH"
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.
volumeUsd, handle may be nullformatUnits(wei, decimals) not formatEther(wei) since USDC projects use 6 decimalsactivityEvents for all-in-one activity streamsprojectMomentswallet query for user positions across all projectssuckerGroup and suckerGroupMomentsnftTiers including SVGs and pricingsuckerTransactions/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