Awesome-omni-skill 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.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/jb-bendystraw-demerzels-lab" ~/.claude/skills/diegosouzapw-awesome-omni-skill-jb-bendystraw-d99c4d && rm -rf "$T"
skills/development/jb-bendystraw-demerzels-lab/SKILL.mdBendystraw: 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 { # 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 }
SuckerGroup Entity (Omnichain Projects)
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!]! }
Participant Entity (Token Holders)
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 }
PayEvent Entity
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 }
CashOutEvent Entity
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 }
NFT Entity
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 }
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! # 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 }
Loan Entity (RevLoans)
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 Entity
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 }
NFTTier Entity
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 }
NFTHook Entity
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 }
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! # Snapshot point block: Int! # Block number timestamp: Int! # Unix timestamp # State at snapshot volume: BigInt! volumeUsd: BigInt! balance: BigInt! trendingScore: BigInt! # Relations project: Project }
SuckerGroupMoment Entity
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 }
SuckerTransaction Entity (Cross-Chain Bridging)
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 }
SendPayoutsEvent Entity
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 }
UseAllowanceEvent Entity
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 }
PermissionHolder Entity
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 }
BorrowLoanEvent Entity
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 }
RepayLoanEvent Entity
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 }
LiquidateLoanEvent Entity
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 }
ReallocateLoanEvent Entity
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 }
BurnEvent Entity
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 }
MintTokensEvent Entity
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 }
ManualMintTokensEvent Entity
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 }
ManualBurnEvent Entity
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 }
MintNftEvent Entity
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 }
DeployErc20Event Entity
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 }
ProjectCreateEvent Entity
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 }
AddToBalanceEvent Entity
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 }
SendPayoutToSplitEvent Entity
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 }
SendReservedTokensToSplitEvent Entity
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 }
SendReservedTokensToSplitsEvent Entity
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 }
AutoIssueEvent Entity
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 }
StoreAutoIssuanceAmountEvent Entity
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 }
DecorateBannyEvent Entity
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 }
CashOutTaxSnapshot Entity
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) }
ParticipantSnapshot Entity (GraphQL)
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! }
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
when querying or displaying projects.version
// 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}`;
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)
// 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') }); } }
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 { // 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.
Filtering by Version
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 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 # 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 } } } }
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: $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 } } }
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: { # 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" }
Sorting
orderBy: "volume" # Field to sort by orderDirection: "desc" # "asc" or "desc"
Sortable Fields by Entity:
| Entity | Sortable Fields |
|---|---|
| Project | , , , , , |
| Participant | , , |
| PayEvent | , |
| CashOutEvent | , |
| NFT | , |
| ActivityEvent | |
| Loan | , , |
| Wallet | , , |
| NFTTier | , , |
| ProjectMoment | , |
| SuckerTransaction | |
| SendPayoutsEvent | , |
| UseAllowanceEvent | , |
| BorrowLoanEvent | , |
| RepayLoanEvent | , |
| LiquidateLoanEvent | |
| BurnEvent | , |
| MintTokensEvent | , |
| MintNftEvent | , |
| DeployErc20Event | |
| ProjectCreateEvent | |
| SendPayoutToSplitEvent | , |
| CashOutTaxSnapshot | |
| ParticipantSnapshot | , |
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:
: Max items to return (default: 100, max: 1000)limit
: Number of items to skipoffset
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; /** * 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);
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.data; } // Convenience methods async getProject(projectId, chainId) { ... } async getSuckerGroup(id) { ... } async getPayments(projectId, chainId, limit) { ... } async getParticipants(projectId, chainId, limit) { ... } async getSnapshot(suckerGroupId, timestamp) { ... } }
Server-Side Proxy
Since the API key must be kept secret, use a server-side proxy:
Next.js API Route
// 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); }
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 |
|---|---|---|
| Invalid API key | Verify API key is correct |
| Too many requests | Add backoff/retry logic |
| GraphQL syntax error | Check query structure |
| Entity doesn't exist | Verify projectId/chainId |
| 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 | |
| JBTokens | |
| JBDirectory | |
| JBSplits | |
V5.0 contracts (for revnets and older projects):
| Contract | Address |
|---|---|
| JBController | |
| JBMultiTerminal | |
| JBRulesets | |
| REVDeployer | |
| JB721TiersHookDeployer | |
V5.1 contracts (for new projects):
| Contract | Address |
|---|---|
| JBController5_1 | |
| JBMultiTerminal5_1 | |
| JBRulesets5_1 | |
| JBOmnichainDeployer5_1 | |
| JB721TiersHookDeployer5_1 | |
Determining project version: Query
JBDirectory.controllerOf(projectId) and compare:
= V5.0 (use JBMultiTerminal, JBRulesets)0x27da30646502e2f642be5281322ae8c394f7668a
= V5.1 (use JBMultiTerminal5_1, JBRulesets5_1)0xf3cc99b11bd73a2e3b8815fb85fe0381b29987e1
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' // 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', }) }
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 |
|---|---|---|---|
| | | |
| | | |
| | | - |
| - | - | - |
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.
SuckerGroup Cross-Chain Aggregation
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:
- Pre-aggregated totals (
,balance
,volume
, etc.)tokenSupply - 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 # 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.
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
may be nullhandle - 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
notformatUnits(wei, decimals)
since USDC projects use 6 decimalsformatEther(wei)
Use Cases
- Project dashboards - Display stats, activity, holders
- Unified activity feeds - Use
for all-in-one activity streamsactivityEvents - Governance snapshots - Get token balances at specific timestamps
- Analytics - Track trends, volumes, contributor growth via
projectMoments - Portfolio tracking - Use
query for user positions across all projectswallet - Omnichain aggregation - Unified view via
andsuckerGroupsuckerGroupMoments - Airdrops - Generate recipient lists from holder data
- Loan dashboards - Track RevLoans borrowing, repayments, liquidations
- NFT galleries - Full tier metadata via
including SVGs and pricingnftTiers - Cross-chain tracking - Monitor token bridging via
suckerTransactions - Treasury management - Track payouts and allowance usage
Related Skills
- Execute multi-chain transactions/jb-relayr
- Build UIs with Bendystraw data/jb-omnichain-ui
- Direct on-chain queries via cast/ethers/jb-query
- RevLoans protocol integration/jb-revloans
- Loan-specific query patterns/jb-loan-queries
- Build NFT galleries with tier data/jb-nft-gallery-ui