Skip to main content

Bendystraw (Data API)

Bendystraw is a GraphQL API for querying Juicebox protocol data across all supported chains. Built on Ponder, it indexes on-chain events and provides real-time access to projects, payments, token holders, NFTs, loans, and more.

Quick Start

Most developers need these:

Endpoints

EnvironmentBase URLChainsStatus
Mainnetbendystraw.xyzEthereum, Arbitrum, Base, OptimismPlayground
Testnettestnet.bendystraw.xyzSepolia, Arbitrum Sepolia, Base Sepolia, Optimism SepoliaPlayground

Authentication

Contact @peripheralist for an API key.

Important: API keys should not be exposed in client-side code. Use a server-side proxy for frontend applications.

Making Requests

GraphQL endpoint:

POST https://<base-url>/<api-key>/graphql

Schema (no auth required):

GET https://bendystraw.xyz/schema

Available Data

Core Entities

EntityDescriptionKey Fields
projectProject configuration and statsprojectId, chainId, name, balance, volume, tokenSupply
participantToken holder balancesaddress, projectId, balance, volume, paymentsCount
suckerGroupCross-chain linked projectsid, projects, balance, tokenSupply, contributorsCount
walletGlobal wallet statsaddress, volume, volumeUsd

Events

EntityDescriptionKey Fields
payEventPayments to projectsamount, beneficiary, memo, newlyIssuedTokenCount
cashOutTokensEventToken redemptionscashOutCount, reclaimAmount, holder
mintTokensEventToken mints (all)tokenCount, beneficiary, reservedPercent
mintNftEventNFT tier mintstierId, tokenId, totalAmountPaid
activityEventUnified event timelinetype, timestamp, projectId

NFTs

EntityDescriptionKey Fields
nftIndividual NFTstokenId, owner, tierId, tokenUri, metadata
nftTierNFT tier configurationtierId, price, initialSupply, remainingSupply
nftHookNFT hook contractsaddress, name, symbol

Loans (Revnet)

EntityDescriptionKey Fields
loanActive loansid, borrowAmount, collateral, owner
borrowLoanEventLoan creationborrowAmount, collateral, beneficiary
repayLoanEventLoan repaymentrepayBorrowAmount, collateralCountToReturn
liquidateLoanEventLoan liquidationborrowAmount, collateral

Common Queries

Get Project Details

query GetProject($projectId: Int!, $chainId: Int!) {
project(projectId: $projectId, chainId: $chainId, version: 5) {
projectId
chainId
name
description
logoUri
balance
volume
volumeUsd
tokenSupply
paymentsCount
contributorsCount
tokenSymbol
owner
suckerGroupId
isRevnet
}
}

List Projects

query ListProjects($chainId: Int!) {
projects(
where: { chainId: $chainId, version: 5 }
orderBy: "createdAt"
orderDirection: "desc"
limit: 20
) {
items {
projectId
chainId
name
logoUri
balance
volume
tokenSymbol
}
totalCount
pageInfo {
hasNextPage
endCursor
}
}
}

Get Project Activity

query GetActivity($projectId: Int!, $chainId: Int!) {
activityEvents(
where: { projectId: $projectId, chainId: $chainId, version: 5 }
orderBy: "timestamp"
orderDirection: "desc"
limit: 50
) {
items {
id
type
timestamp
txHash
from

# Include specific event data
payEvent {
amount
amountUsd
beneficiary
memo
newlyIssuedTokenCount
}
cashOutTokensEvent {
cashOutCount
reclaimAmount
holder
}
mintNftEvent {
tierId
tokenId
totalAmountPaid
}
}
totalCount
}
}

Get Token Holders

query GetParticipants($projectId: Int!, $chainId: Int!) {
participants(
where: { projectId: $projectId, chainId: $chainId, version: 5 }
orderBy: "balance"
orderDirection: "desc"
limit: 100
) {
items {
address
balance
creditBalance
erc20Balance
volume
volumeUsd
paymentsCount
lastPaidTimestamp
}
totalCount
}
}

Get Cross-Chain (Omnichain) Data

For projects deployed across multiple chains via suckers:

query GetSuckerGroup($suckerGroupId: String!) {
suckerGroup(id: $suckerGroupId) {
id
projects
balance
tokenSupply
volume
volumeUsd
contributorsCount
paymentsCount
}
}

# Get all activity across chains
query GetOmnichainActivity($suckerGroupId: String!) {
activityEvents(
where: { suckerGroupId: $suckerGroupId, version: 5 }
orderBy: "timestamp"
orderDirection: "desc"
limit: 50
) {
items {
chainId
type
timestamp
payEvent {
amount
beneficiary
}
}
}
}

Get NFT Tiers

query GetNftTiers($projectId: Int!, $chainId: Int!) {
nftTiers(
where: { projectId: $projectId, chainId: $chainId, version: 5 }
orderBy: "tierId"
orderDirection: "asc"
) {
items {
tierId
price
initialSupply
remainingSupply
resolvedUri
metadata
category
}
}
}

Get User's NFTs

query GetUserNfts($owner: String!, $projectId: Int!, $chainId: Int!) {
nfts(
where: { owner: $owner, projectId: $projectId, chainId: $chainId, version: 5 }
) {
items {
tokenId
tierId
tokenUri
metadata
createdAt
tier {
price
resolvedUri
}
}
}
}

Get Loans (Revnet)

query GetLoans($projectId: Int!, $chainId: Int!) {
loans(
where: { projectId: $projectId, chainId: $chainId, version: 5 }
orderBy: "createdAt"
orderDirection: "desc"
) {
items {
id
borrowAmount
collateral
owner
beneficiary
prepaidDuration
prepaidFeePercent
createdAt
}
}
}
query TrendingProjects($chainId: Int!) {
projects(
where: { chainId: $chainId, version: 5 }
orderBy: "trendingScore"
orderDirection: "desc"
limit: 10
) {
items {
projectId
name
logoUri
trendingScore
trendingVolume
trendingPaymentsCount
}
}
}

SDK Integration

Using with juice-sdk-react

The SDK provides a useBendystrawQuery hook for easy data fetching with caching and polling:

import { useBendystrawQuery } from 'juice-sdk-react';
import { gql } from 'graphql-request';

// Define your query
const PROJECT_QUERY = gql`
query GetProject($projectId: Int!, $chainId: Int!) {
project(projectId: $projectId, chainId: $chainId, version: 5) {
name
balance
volume
tokenSupply
contributorsCount
}
}
`;

function ProjectStats({ projectId, chainId }) {
const { data, isLoading, error } = useBendystrawQuery(
PROJECT_QUERY,
{ projectId: Number(projectId), chainId: Number(chainId) },
{ pollInterval: 30000 } // Poll every 30 seconds
);

if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

return (
<div>
<h2>{data?.project?.name}</h2>
<p>Balance: {formatEther(data?.project?.balance || 0n)} ETH</p>
<p>Contributors: {data?.project?.contributorsCount}</p>
</div>
);
}

Setup with JBProjectProvider

Configure Bendystraw in your provider setup:

import { JBProjectProvider } from 'juice-sdk-react';

function App({ projectId, chainId }) {
return (
<JBProjectProvider
projectId={BigInt(projectId)}
chainId={chainId}
ctxProps={{
bendystraw: {
apiKey: process.env.NEXT_PUBLIC_BENDYSTRAW_KEY
}
}}
>
<YourApp />
</JBProjectProvider>
);
}

Complete Activity Feed Example

'use client';

import { useBendystrawQuery } from 'juice-sdk-react';
import { formatEther } from 'viem';
import { gql } from 'graphql-request';

const ACTIVITY_QUERY = gql`
query GetActivity($projectId: Int!, $chainId: Int!) {
activityEvents(
where: { projectId: $projectId, chainId: $chainId, version: 5 }
orderBy: "timestamp"
orderDirection: "desc"
limit: 20
) {
items {
id
type
timestamp
txHash
from
payEvent {
amount
beneficiary
memo
}
cashOutTokensEvent {
cashOutCount
reclaimAmount
}
mintNftEvent {
tierId
tokenId
}
}
}
}
`;

function ActivityFeed({ projectId, chainId }) {
const { data, isLoading } = useBendystrawQuery(
ACTIVITY_QUERY,
{ projectId: Number(projectId), chainId: Number(chainId) },
{ pollInterval: 10000 }
);

if (isLoading) return <div>Loading activity...</div>;

const events = data?.activityEvents?.items || [];

return (
<div className="activity-feed">
<h3>Recent Activity</h3>
{events.map((event) => (
<div key={event.id} className="activity-item">
<span className="type">{formatEventType(event.type)}</span>
<span className="time">
{new Date(event.timestamp * 1000).toLocaleString()}
</span>

{event.type === 'payEvent' && event.payEvent && (
<div className="details">
<p>
{truncateAddress(event.payEvent.beneficiary)} paid{' '}
{formatEther(BigInt(event.payEvent.amount))} ETH
</p>
{event.payEvent.memo && <p className="memo">"{event.payEvent.memo}"</p>}
</div>
)}

{event.type === 'cashOutTokensEvent' && event.cashOutTokensEvent && (
<div className="details">
<p>
Redeemed {formatEther(BigInt(event.cashOutTokensEvent.cashOutCount))} tokens
for {formatEther(BigInt(event.cashOutTokensEvent.reclaimAmount))} ETH
</p>
</div>
)}

{event.type === 'mintNftEvent' && event.mintNftEvent && (
<div className="details">
<p>Minted NFT #{event.mintNftEvent.tokenId} (Tier {event.mintNftEvent.tierId})</p>
</div>
)}

<a
href={`https://etherscan.io/tx/${event.txHash}`}
target="_blank"
className="tx-link"
>
View tx
</a>
</div>
))}
</div>
);
}

function formatEventType(type) {
const labels = {
payEvent: 'Payment',
cashOutTokensEvent: 'Redemption',
mintNftEvent: 'NFT Mint',
mintTokensEvent: 'Token Mint',
sendPayoutsEvent: 'Payout',
borrowLoanEvent: 'Loan',
};
return labels[type] || type;
}

function truncateAddress(addr) {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
}

Token Holders Leaderboard

'use client';

import { useBendystrawQuery } from 'juice-sdk-react';
import { formatEther } from 'viem';
import { gql } from 'graphql-request';

const HOLDERS_QUERY = gql`
query GetHolders($projectId: Int!, $chainId: Int!) {
participants(
where: { projectId: $projectId, chainId: $chainId, version: 5 }
orderBy: "balance"
orderDirection: "desc"
limit: 50
) {
items {
address
balance
volume
volumeUsd
paymentsCount
}
totalCount
}
}
`;

function HoldersLeaderboard({ projectId, chainId, tokenSymbol }) {
const { data, isLoading } = useBendystrawQuery(
HOLDERS_QUERY,
{ projectId: Number(projectId), chainId: Number(chainId) }
);

if (isLoading) return <div>Loading holders...</div>;

const holders = data?.participants?.items || [];
const totalHolders = data?.participants?.totalCount || 0;

return (
<div className="leaderboard">
<h3>Top Holders ({totalHolders} total)</h3>
<table>
<thead>
<tr>
<th>Rank</th>
<th>Address</th>
<th>Balance</th>
<th>Contributed</th>
</tr>
</thead>
<tbody>
{holders.map((holder, i) => (
<tr key={holder.address}>
<td>{i + 1}</td>
<td>{truncateAddress(holder.address)}</td>
<td>
{formatEther(BigInt(holder.balance))} {tokenSymbol}
</td>
<td>{formatEther(BigInt(holder.volume))} ETH</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

Special Endpoints

Participants Snapshot

Get all token holder balances at a specific timestamp (useful for airdrops, voting snapshots):

// POST https://<base-url>/<api-key>/participants
const response = await fetch(`https://bendystraw.xyz/${API_KEY}/participants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
suckerGroupId: 'your-sucker-group-id',
timestamp: 1704067200, // Unix timestamp in seconds
}),
});

const participants = await response.json();
// Returns: [{ address, balance, creditBalance, erc20Balance, volume, ... }]

Key Concepts

Chain IDs

Every entity includes a chainId field. Use this for filtering:

ChainID
Ethereum1
Optimism10
Arbitrum42161
Base8453
Sepolia11155111
Optimism Sepolia11155420
Arbitrum Sepolia421614
Base Sepolia84532

Sucker Groups (Omnichain)

Projects linked across chains share a suckerGroupId. Query by this ID to get aggregated cross-chain data:

# Get total stats across all chains
query {
suckerGroup(id: "your-sucker-group-id") {
balance # Total balance (all chains)
tokenSupply # Total supply (all chains)
contributorsCount
}
}

Version Field

Always include version: 5 in queries for V5 protocol data. The version field is part of most compound primary keys.

Deterministic IDs

  • project.id = computed from projectId, version, and chainId
  • suckerGroup.id = hash of contained project IDs

These IDs are stable and can be stored/cached. All other IDs may change on reindexing.


Pagination

Use cursor-based pagination for large result sets:

query GetPayments($cursor: String) {
payEvents(
where: { projectId: 1, chainId: 1, version: 5 }
orderBy: "timestamp"
orderDirection: "desc"
limit: 50
after: $cursor
) {
items {
id
amount
beneficiary
timestamp
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}

Then fetch the next page:

const nextPage = await query({
cursor: data.payEvents.pageInfo.endCursor
});

Resources