Skip to content

Commit 389834c

Browse files
authored
Merge pull request #179 from carbonplan/katamartin/offsets-db-additions
Add OffsetsDB additions post
2 parents 44f67d9 + 15d44cd commit 389834c

File tree

7 files changed

+400
-1
lines changed

7 files changed

+400
-1
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
steps:
2525
- uses: actions/checkout@v1
2626
- uses: actions/setup-python@v2.3.1
27-
- uses: pre-commit/action@v2.0.3
27+
- uses: pre-commit/action@v3.0.1
2828
- name: Set up Node.js 18.x
2929
uses: actions/setup-node@v2.5.1
3030
with:

components/mdx/page-components.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import dynamic from 'next/dynamic'
33
// NOTE: This is a dynamically generated file based on the config specified under the
44
// `components` key in each post's frontmatter.
55
const components = {
6+
'offsets-db-additions': {
7+
BeneficiarySearch: dynamic(() =>
8+
import('../../posts/offsets-db-additions/beneficiary-search.js').then(
9+
(mod) => mod.BeneficiarySearch || mod.default
10+
)
11+
),
12+
ProjectTypeSummary: dynamic(() =>
13+
import('../../posts/offsets-db-additions/project-type-summary.js').then(
14+
(mod) => mod.ProjectTypeSummary || mod.default
15+
)
16+
),
17+
},
618
'counterfactual-accounting-update': {
719
AccountingGraph: dynamic(() =>
820
import(

package-lock.json

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"react-dom": "^18.2.0",
5555
"rehype-slug": "^4.0.1",
5656
"remark-mdx-code-meta": "^2.0.0",
57+
"swr": "^2.3.3",
5758
"theme-ui": "^0.15.3",
5859
"topojson-client": "^3.1.0",
5960
"zarr-js": "^1.0.0"

posts/offsets-db-additions.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
version: 1.0.0
3+
title: We’ve added retirement user and project type data to OffsetsDB
4+
authors:
5+
- Grayson Badgley
6+
- Anderson Banihirwe
7+
- Kata Martin
8+
date: 04-23-2025
9+
summary: We’ve expanded our database of offset data with searchable project types and details about who is using offset credits.
10+
components:
11+
- name: BeneficiarySearch
12+
src: ./beneficiary-search.js
13+
- name: ProjectTypeSummary
14+
src: ./project-type-summary.js
15+
---
16+
17+
We’ve expanded the functionality of OffsetsDB, our database that organizes offset data from five of the largest offset registries. Starting today, you can access two new types of data: standardized information about the organizations and companies using offsets, and improved information about project types.
18+
19+
Adding the retirement user data is a big change. Hardly a week goes by without someone emailing us asking how to figure out who benefits from retiring the offset credits sold by a particular project. It’s such a common question because it’s such an important question. A transparent offset market requires that we’re able to trace offset credits to companies and the specific claims (e.g., net zero) those companies make.
20+
21+
Unfortunately, the global carbon market doesn’t always make that easy.
22+
23+
Part of the problem is that there’s no requirement to disclose who’s using which offsets. Anyone who uses a voluntary offset to make an environmental claim has the _option_ to publicly disclose that credit retirement. But most don’t. Of 1.34 billion credit retirements tracked in OffsetsDB, only 0.67 billion include information about who benefits from that retirement. In other words, we know next to nothing about who used roughly half the offset credits within the voluntary carbon market.
24+
25+
And the half we do have data for is a bit of a mess. Rather than existing in a single place, every registry has its own version of user data, with no consistency between how different registries keep their records. In fact, even individual registries don’t organize their own retirement user data in a consistent way.
26+
27+
Say you want to know how many credits Delta, the commercial airline, has retired. First, you have to gather retirement data from all the registries — there’s no guarantee Delta will have only purchased from one. Then you have to run multiple searches of those registries’ data, using a variety of different possible ways of writing the airline’s name. A retirement could be linked to “Delta,” or “Delta Airlines,” or “Delta Air Lines.” In fact, Delta often retires credits under “DL,” which is its airline code used for its flight numbers. Lack of standard schema makes it painstaking to answer even simple questions about trends in offset usage. It also doesn’t help that we’re constantly getting new retirement data with no guarantees about what format _that_ data will use.
28+
29+
Suffice it to say, none of this is good for market transparency.
30+
31+
<Figure>
32+
<BeneficiarySearch />
33+
</Figure>
34+
35+
Our updates to OffsetsDB bring both increased standardization and searchability to user data. For this initial release, we used a mostly manual process to pull information from offset registries and fit it to a common format. While we’ve automated parts of the process, we foresee some amount of manual intervention going forward. In fact, this first release only standardizes a little over two-thirds of the 0.67 billion classifiable credits in the database.
36+
37+
It’s a big lift, but we think it’s worth the ongoing effort. Claims backed by offsets can’t be taken seriously if there’s no way to tie them to specific credits that come from specific projects. With that in mind, we’re excited to see standards bodies within the carbon market, such as ICVCM and SBTi, start to advocate for full, complete, and standardized disclosures of retirement user data. Hopefully, over the long term, this will make user data less messy — and make our job easier.
38+
39+
Our other change to OffsetsDB is smaller in scope, but still important. Previously, you could search OffsetsDB by project categories — broad groupings of projects, like Forests or Renewable Energy. We’ve now added project types that further refine those categories, allowing more precise searches. So rather than looking at everything categorized as Forest, you can now distinguish reforestation projects from improved forest management projects.
40+
41+
<Figure>
42+
<ProjectTypeSummary />
43+
</Figure>
44+
45+
While the distinction between categories and types might seem minor, it takes a fair amount of work to assign projects to their appropriate type. OffsetsDB originally sorted projects into categories based on their protocol. But a protocol such as ACM0002 — Grid-connected Electricity Generation from Renewable Sources — could include electricity generated by wind, solar, or hydroelectric facilities. To sort these projects by type, we have to turn to project specific paperwork. And there is a lot of paperwork to go through — OffsetsDB tracks more than 10,000 projects.
46+
47+
Thankfully, we didn’t have to start from scratch. That’s because the folks over at the [Berkeley Carbon Trading Project](https://gspp.berkeley.edu/research-and-impact/centers/cepp/projects/berkeley-carbon-trading-project) maintain a similar offsets database that already contains detailed information about project types. And, what’s more, they publish their project-type information using a [permissive license](https://creativecommons.org/licenses/by/4.0/), which means we can freely incorporate their work into OffsetsDB. That’s wonderful because project-type data is the sort of thing you only need to create once. This underscores the value we see in open science — once someone classifies a project and shares the data, no one else should ever have to do that work again.
48+
49+
As with previous installments of OffsetsDB, you’re free to access the data both through our the browser-based [database tool](https://carbonplan.org/research/offsets-db) or by [downloading the data](https://offsets-db-data.readthedocs.io/en/latest/data-access.html) and playing around with it yourself. Keep track of our progress on [GitHub](https://github.com/carbonplan/offsets-db-data) or reach out at [hello@carbonplan.org](mailto:hello@carbonplan.org) if you have any questions.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { Box, Flex, IconButton } from 'theme-ui'
2+
import { Row, Column, Input, Button } from '@carbonplan/components'
3+
import { RotatingArrow, Search, X } from '@carbonplan/icons'
4+
import { useEffect, useState } from 'react'
5+
import { format } from 'd3-format'
6+
import useSWR from 'swr'
7+
8+
export const formatValue = (value) => {
9+
if (value < 1000) {
10+
return value
11+
} else {
12+
let result = format('.3s')(value)
13+
if (value >= 1e9) {
14+
return result.replace('G', 'B')
15+
}
16+
return result.toUpperCase(0)
17+
}
18+
}
19+
20+
const sx = {
21+
row: {
22+
borderStyle: 'solid',
23+
borderWidth: '0px',
24+
borderTopWidth: '1px',
25+
borderColor: 'muted',
26+
height: '100%',
27+
py: [3, 3, 3, 4],
28+
},
29+
label: {
30+
fontFamily: 'heading',
31+
letterSpacing: 'smallcaps',
32+
textTransform: 'uppercase',
33+
fontSize: [2, 2, 2, 3],
34+
},
35+
value: {
36+
fontFamily: 'faux',
37+
letterSpacing: 'faux',
38+
textTransform: 'uppercase',
39+
color: 'purple',
40+
fontSize: [3, 3, 3, 4],
41+
},
42+
}
43+
44+
async function getTransactions([, search]) {
45+
try {
46+
const reqUrl = new URL(
47+
`https://carbonplan.org/research/offsets-db/api/query`
48+
)
49+
const params = new URLSearchParams()
50+
params.append('path', 'credits')
51+
params.append('per_page', 1)
52+
params.append('transaction_type', 'retirement')
53+
if (search) {
54+
params.append('beneficiary_search', search.trim())
55+
params.append(
56+
'beneficiary_search_fields',
57+
'retirement_beneficiary_harmonized'
58+
)
59+
params.append('beneficiary_search_fields', 'retirement_account')
60+
params.append('beneficiary_search_fields', 'retirement_beneficiary')
61+
params.append('beneficiary_search_fields', 'retirement_note')
62+
params.append('beneficiary_search_fields', 'retirement_reason')
63+
}
64+
reqUrl.search = params.toString()
65+
const serverRes = await fetch(reqUrl, {
66+
credentials: 'include',
67+
})
68+
if (!serverRes.ok) {
69+
throw new Error(
70+
`API request failed: ${serverRes.status} ${serverRes.statusText}`
71+
)
72+
}
73+
const result = await serverRes.json()
74+
return result.pagination.total_entries
75+
} catch (e) {
76+
throw new Error('Unexpected error')
77+
}
78+
}
79+
80+
export const useDebounce = (value, delay = 100) => {
81+
const [debounced, setDebounced] = useState(value)
82+
83+
useEffect(() => {
84+
const timeoutID = setTimeout(() => {
85+
setDebounced(value)
86+
}, delay)
87+
88+
return () => {
89+
clearTimeout(timeoutID)
90+
}
91+
}, [value, delay])
92+
93+
return debounced
94+
}
95+
96+
const BeneficiarySearch = () => {
97+
const [search, setSearch] = useState('Delta Airlines')
98+
const { data: transactions, isLoading } = useSWR(
99+
['/credits', useDebounce(search)],
100+
getTransactions,
101+
{
102+
revalidateOnFocus: false,
103+
revalidateIfStale: false,
104+
}
105+
)
106+
return (
107+
<>
108+
<Row columns={6} sx={sx.row}>
109+
<Column start={1} width={[3, 2, 2, 2]}>
110+
<Box sx={sx.label}>
111+
<Flex
112+
as='label'
113+
htmlFor='beneficiary_search'
114+
sx={{ gap: 2, alignItems: 'center' }}
115+
>
116+
User <Search sx={{ width: 14 }} />
117+
</Flex>
118+
</Box>
119+
</Column>
120+
<Column start={[4, 3, 3, 3]} width={[3, 4, 4, 4]}>
121+
<Flex sx={{ justifyContent: 'space-between' }}>
122+
<Input
123+
id='beneficiary_search'
124+
placeholder='enter a search term'
125+
color='purple'
126+
value={search}
127+
onChange={(e) => setSearch(e.target.value)}
128+
sx={{
129+
...(search ? sx.value : { fontFamily: 'mono', fontSize: 2 }),
130+
height: '22px',
131+
textTransform: 'none',
132+
borderBottom: 0,
133+
borderColor: 'muted',
134+
flexGrow: 1,
135+
}}
136+
/>
137+
<IconButton
138+
sx={{
139+
cursor: 'pointer',
140+
p: [0],
141+
mt: ['3px', '3px', '3px', '4px'],
142+
mx: [2],
143+
width: 18,
144+
height: 18,
145+
flexShrink: 0,
146+
}}
147+
onKeyDown={(e) => {
148+
if (e.key === 'Enter') {
149+
setSearch('')
150+
}
151+
}}
152+
onClick={() => {
153+
setSearch('')
154+
}}
155+
>
156+
<X
157+
sx={{
158+
strokeWidth: 1.5,
159+
color: 'secondary',
160+
transition: 'stroke 0.15s',
161+
'@media (hover: hover) and (pointer: fine)': {
162+
'&:hover': {
163+
stroke: 'primary',
164+
},
165+
},
166+
}}
167+
/>
168+
</IconButton>
169+
</Flex>
170+
</Column>
171+
</Row>
172+
<Row columns={6} sx={sx.row}>
173+
<Column start={1} width={[3, 2, 2, 2]}>
174+
<Box sx={sx.label}>Retirements</Box>
175+
</Column>
176+
<Column start={[4, 3, 3, 3]} width={[3, 4, 4, 4]}>
177+
<Flex
178+
sx={{
179+
height: '100%',
180+
alignItems: 'baseline',
181+
gap: 2,
182+
mt: [0, 0, 0, '-6px'],
183+
}}
184+
>
185+
<Box sx={sx.value}>
186+
{!isLoading && typeof transactions === 'number'
187+
? formatValue(transactions)
188+
: '—'}
189+
</Box>
190+
<Box sx={{ fontFamily: 'mono' }}>
191+
{transactions === 1 ? 'transaction' : 'transactions'}
192+
</Box>
193+
</Flex>
194+
</Column>
195+
</Row>
196+
<Row columns={6} sx={{ ...sx.row, pb: 0 }}>
197+
<Column start={[4, 3, 3, 3]} width={[3, 4, 4, 4]}>
198+
<Button
199+
suffix={<RotatingArrow />}
200+
size='xs'
201+
inverted
202+
href={`https://carbonplan.org/research/offsets-db/transactions${
203+
search ? `?beneficiary=${search}` : ''
204+
}`}
205+
>
206+
View in OffsetsDB
207+
</Button>
208+
</Column>
209+
</Row>
210+
</>
211+
)
212+
}
213+
214+
export default BeneficiarySearch

0 commit comments

Comments
 (0)