Contribution Graph
The contribution graph is a GitHub-style heatmap that visualizes your daily bit completions. Each cell represents a day, with intensity levels (0-4) computed from completion counts. Use it to visualize execution consistency, track streaks, and prove productive output.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| GET | /contribution-graph | Get contribution graph data | JWT | 60/min |
GET /contribution-graph
Returns daily completion data with intensity levels and streak information. Defaults to the last 365 days. Filter by chainy or chain for scoped graphs.
Authentication: JWT Bearer token required. Rate limit: 60/min
Request
Query Parameters:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
from | string | No | 365 days ago | Start date |
to | string | No | Today | End date |
chainyId | string | No | -- | Filter to chains within a chainy |
chainId | string | No | -- | Filter to a single chain |
If both chainyId and chainId are provided, chainId takes precedence.
Response
Response Example
{
"data": {
"cells": [
{ "date": "2026-03-15", "count": 0, "level": 0 },
{ "date": "2026-03-16", "count": 3, "level": 2 },
{ "date": "2026-03-17", "count": 5, "level": 4 }
],
"streaks": {
"current": 2,
"longest": 14
},
"total": 87
}
}Response Fields
| Field | Type | Description |
|---|---|---|
cells | array | One entry per day in the date range |
cells[].date | string | Date in YYYY-MM-DD format |
cells[].count | number | Number of bit completions on this day |
cells[].level | number | Intensity level (0-4) |
streaks | object | Streak information |
streaks.current | number | Current consecutive-day streak |
streaks.longest | number | Longest streak in the queried range |
total | number | Total completions across the entire range |
Code Examples
curl "https://api.chainabit.com/api/v1/contribution-graph?from=2026-01-01&to=2026-03-17" \
-H "Authorization: Bearer $TOKEN"const res = await fetch(
`${BASE_URL}/contribution-graph?from=2026-01-01&to=2026-03-17`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { data } = await res.json();import requests
res = requests.get(
f"{BASE_URL}/contribution-graph",
params={"from": "2026-01-01", "to": "2026-03-17"},
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]Chainy-Scoped Example
Use the
idfrom Create Chainy's response as$CHAINY_ID.
curl "https://api.chainabit.com/api/v1/contribution-graph?chainyId=$CHAINY_ID" \
-H "Authorization: Bearer $TOKEN"const CHAINY_ID = process.env.CHAINY_ID; // from Create Chainy's response
const res = await fetch(
`${BASE_URL}/contribution-graph?chainyId=${CHAINY_ID}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { data } = await res.json();import requests, os
chainy_id = os.environ["CHAINY_ID"] # from Create Chainy's response
res = requests.get(
f"{BASE_URL}/contribution-graph",
params={"chainyId": chainy_id},
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]Level Semantics
Levels use quartile-based thresholds relative to the user's peak activity in the queried range:
| Level | Meaning | Threshold |
|---|---|---|
| 0 | No activity | count === 0 |
| 1 | Light activity | count <= 25% of peak |
| 2 | Moderate activity | count <= 50% of peak |
| 3 | High activity | count <= 75% of peak |
| 4 | Peak activity | count > 75% of peak |
Levels are always relative to the user's own maximum daily count in the range. A user averaging 2 completions/day and a user averaging 20 completions/day both see the full 0-4 range based on their own data.
Scoping Summary
| Query Param | Scope | Description |
|---|---|---|
| Account | All completions for the authenticated user | |
chainyId | Chainy | Completions for chains within the specified chainy |
chainId | Chain | Completions for a single chain |
Full Year Example
Request the full year to render a complete heatmap:
curl "https://api.chainabit.com/api/v1/contribution-graph?from=2025-03-17&to=2026-03-17" \
-H "Authorization: Bearer $TOKEN"const from = new Date();
from.setFullYear(from.getFullYear() - 1);
const res = await fetch(
`${BASE_URL}/contribution-graph?from=${from.toISOString().slice(0, 10)}&to=${new Date().toISOString().slice(0, 10)}`,
{ headers: { Authorization: `Bearer ${TOKEN}` } }
);
const { data } = await res.json();
// data.cells has 365-366 entriesimport requests
from datetime import datetime, timedelta
today = datetime.utcnow()
one_year_ago = today - timedelta(days=365)
res = requests.get(
f"{BASE_URL}/contribution-graph",
params={
"from": one_year_ago.strftime("%Y-%m-%d"),
"to": today.strftime("%Y-%m-%d"),
},
headers={"Authorization": f"Bearer {TOKEN}"},
)
data = res.json()["data"]
# data["cells"] has 365-366 entries{
"data": {
"cells": [
{ "date": "2025-03-17", "count": 1, "level": 1 },
{ "date": "2025-03-18", "count": 0, "level": 0 },
"... (365 entries total)",
{ "date": "2026-03-17", "count": 5, "level": 4 }
],
"streaks": {
"current": 23,
"longest": 45
},
"total": 892
}
}