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​