Complete reference for the CarbonLedger credit lifecycle — actors, contract functions, on-chain data, off-chain data, error conditions, and edge cases for each stage.
- Overview
- Actors
- Sequence Diagram
- Stage 1 — Project Registration
- Stage 2 — Verification
- Stage 3 — Oracle Monitoring
- Stage 4 — Credit Minting
- Stage 5 — Marketplace Listing
- Stage 6 — Purchase
- Stage 7 — Retirement
- Stage 8 — Certification
- Error Reference
A carbon credit travels through eight stages from a real-world offset project to a permanent, publicly verifiable retirement record. Each stage maps to one or more Soroban contract calls and database writes. The lifecycle is designed so that fraud, double-counting, and greenwashing are prevented at the smart-contract level — not just by policy.
Registration → Verification → Oracle Monitoring → Minting
↓ ↓ ↓ ↓
Pending Verified Data on-chain Serial numbers
assigned
↓
Marketplace Listing
↓
Purchase
↓
Retirement ←── IRREVERSIBLE
↓
Certification
(permanent public URL)
| Actor | Role |
|---|---|
| Project Developer | Registers projects, requests credit minting, receives USDC payments |
| Accredited Verifier | Approves or rejects projects; submits on-chain attestations |
| Oracle Service | Python bridge that posts satellite monitoring data and price feeds to contracts |
| Corporation | Buys credits from the marketplace and retires them for ESG offset claims |
| Admin | Deploys and upgrades contracts; maintains the verifier allow-list |
| Public / Auditor | Read-only access to audit trail — no wallet required |
sequenceDiagram
actor Dev as Project Developer
actor Ver as Verifier
participant Oracle as Oracle Service<br/>(Python)
participant Reg as carbon_registry
participant Cred as carbon_credit
participant Mkt as carbon_marketplace
participant OraC as carbon_oracle
participant DB as PostgreSQL
participant IPFS as IPFS (Pinata)
actor Corp as Corporation
Dev->>IPFS: Upload project documentation
IPFS-->>Dev: metadata_cid
Dev->>Reg: register_project(project_id, metadata_cid, ...)
Reg-->>DB: status = Pending
Ver->>Reg: verify_project(verifier_address, project_id)
Reg-->>DB: status = Verified
loop Every 6 hours
Oracle->>Ver: Poll Gold Standard / Verra VCS APIs
Ver-->>Oracle: Monitoring report
Oracle->>IPFS: Upload satellite imagery
IPFS-->>Oracle: satellite_cid
Oracle->>OraC: submit_monitoring_data(project_id, period, tonnes_verified, satellite_cid, sig, nonce)
OraC-->>DB: MonitoringData stored
end
loop Every 12 hours
Oracle->>OraC: update_credit_price(methodology, vintage_year, price_usdc, sig, nonce)
end
Dev->>Cred: mint_credits(project_id, amount, serial_start, serial_end, ...)
Cred->>OraC: is_monitoring_current(project_id)
OraC-->>Cred: true
Cred->>Cred: verify_serial_range(serial_start, serial_end)
Cred-->>DB: CreditBatch created (status = Active)
Dev->>Mkt: list_credits(listing_id, batch_id, amount, price_per_credit_usdc, ...)
Mkt-->>DB: MarketListing created (status = Active)
Corp->>Mkt: purchase_credits(listing_id, amount)
Mkt->>Cred: transfer_credits(seller, buyer, batch_id, amount)
Mkt->>USDC: transfer(buyer → seller, total_cost)
Mkt-->>DB: listing status = Sold / PartiallyFilled
Corp->>IPFS: Upload certificate metadata
IPFS-->>Corp: certificate_cid
Corp->>Cred: retire_credits(batch_id, amount, retirement_id, beneficiary, reason, certificate_cid)
Note over Cred: PERMANENTLY IRREVERSIBLE
Cred-->>DB: RetirementRecord created
Cred-->>DB: CreditBatch status = FullyRetired / PartiallyRetired
DB->>IPFS: Generate and pin PDF certificate
IPFS-->>Corp: Public certificate URL (permanent)
Summary: A project developer submits a new carbon offset project for review. The project is stored on-chain with Pending status and cannot issue credits until a verifier approves it.
| Actor | Action |
|---|---|
| Project Developer | Fills registration form with project details and uploads documentation |
| Admin | Signs the register_project transaction (acts as contract gatekeeper) |
Contract: carbon_registry
Function: register_project
register_project(
env: Env,
admin: Address,
project_id: String, // globally unique identifier (e.g. "REDD-2024-BRA-001")
name: String,
metadata_cid: String, // IPFS CID pointing to project documentation
verifier_address: Address,
methodology: String, // e.g. "REDD+", "VCS-VM0015", "Gold Standard"
country: String, // ISO 3166-1 alpha-3
project_type: String, // e.g. "reforestation", "blue_carbon", "methane_capture"
vintage_year: u32, // baseline year (e.g. 2023)
)Stored in carbon_registry contract storage under key Project(project_id):
CarbonProject {
project_id: String,
name: String,
methodology: String,
country: String,
project_type: String,
verifier_address: Address,
metadata_cid: String, // IPFS CID
total_credits_issued: i128, // starts at 0
total_credits_retired: i128, // starts at 0
methodology_score: u32, // 0–100; must be ≥ 70 to advance
status: ProjectStatus::Pending,
vintage_year: u32,
created_at: u64, // ledger timestamp
}| Store | Table / Object | Fields |
|---|---|---|
| PostgreSQL | CarbonProject |
id, projectId, name, description, methodology, country, projectType, status, vintageYear, methodologyScore, metadataCid, verifierAddress, ownerAddress, coordinates (JSON), createdAt |
| IPFS | Project documentation bundle | Project feasibility study, methodology application, land title or legal proof, baseline survey data |
| Error Code | Constant | Trigger |
|---|---|---|
| 17 | ProjectAlreadyExists |
A project with the same project_id is already registered |
| 20 | MethodologyScoreLow |
Computed methodology_score is below 70 |
| 19 | AlreadyInitialized |
Contract initialize() called a second time |
- Duplicate project IDs are rejected at the contract level; the backend generates IDs using a combination of methodology code, vintage year, country, and a UUID suffix.
- Metadata CID validation is done off-chain before submission — the backend verifies the CID resolves on the IPFS gateway before calling
register_project. - Verifier assignment happens at registration time and cannot be changed without admin intervention.
Summary: An accredited verifier reviews the project documentation and either approves or rejects it. Approval is an on-chain transaction that moves the project from Pending to Verified. Only verifiers on the contract's allow-list can call this function.
| Actor | Action |
|---|---|
| Accredited Verifier | Reviews off-chain docs, calls verify_project or reject_project |
| Oracle Service | verification_listener.py polls Gold Standard and Verra VCS APIs every 6 hours for automated attestation signals |
Contract: carbon_registry
// Approve
verify_project(
env: Env,
verifier_address: Address, // must be in the Verifiers allow-list
project_id: String,
)
// Reject (permanent)
reject_project(
env: Env,
verifier_address: Address,
project_id: String,
reason: String, // stored on-chain for transparency
)
// Administrative hold
suspend_project(
env: Env,
admin: Address,
project_id: String,
reason: String,
)The CarbonProject.status field is updated in-place:
| Outcome | status value |
|---|---|
| Approved | ProjectStatus::Verified |
| Rejected | ProjectStatus::Rejected |
| Suspended | ProjectStatus::Suspended |
The reason string for rejection/suspension is stored in the contract event log.
| Store | Table / Object | Fields Updated |
|---|---|---|
| PostgreSQL | CarbonProject |
status, updatedAt |
| PostgreSQL | AuditLog |
actor, action, project_id, reason, timestamp |
| Error Code | Constant | Trigger |
|---|---|---|
| 1 | ProjectNotFound |
project_id does not exist in contract storage |
| 7 | UnauthorizedVerifier |
Caller address is not in the Verifiers storage list |
| 3 | ProjectSuspended |
Attempting to verify a project that is already suspended |
- Re-verification after rejection is not possible — a rejected project must be re-registered with a new
project_idand corrected documentation. - Verifier rotation — the verifier allow-list is managed by admin. If a verifier's accreditation lapses, admin removes them and the assigned projects must be re-assigned.
- Automated attestation —
verification_listener.pydetects when a project has received a positive signal from a Gold Standard or Verra VCS API and can triggerverify_projectautomatically. Manual override is always available.
Summary: The oracle service continuously collects real-world monitoring data (satellite imagery, methodology assessments) and posts it on-chain. This data is a prerequisite for minting — credits cannot be issued against stale monitoring data older than 365 days.
| Actor | Action |
|---|---|
| Oracle Service | verification_listener.py (6-hour poll), satellite_monitor.py (webhook receiver) |
| Satellite Providers | Google Earth Engine sends webhook events with imagery; Planet Labs API provides independent validation |
| Oracle Service | price_oracle.py posts benchmark prices every 12 hours |
Contract: carbon_oracle
// Submit monitoring data for a project period
submit_monitoring_data(
env: Env,
oracle_signer: Address,
project_id: String,
period: String, // e.g. "2024-Q1", "2024-H1", "2024"
tonnes_verified: i128, // CO2e tonnes measured for this period
methodology_score: u32, // recalculated per-period (0–100)
satellite_cid: String, // IPFS CID of satellite imagery bundle
signature: BytesN<64>, // Ed25519 signature over (project_id + period + nonce)
nonce: u64, // monotonically increasing, prevents replay attacks
)
// Push benchmark price for a methodology + vintage combination
update_credit_price(
env: Env,
oracle_signer: Address,
methodology: String,
vintage_year: u32,
price_usdc: i128, // in stroops: 1 USDC = 10_000_000 stroops
signature: BytesN<64>,
nonce: u64,
)
// Flag a project for investigation (triggers suspension workflow)
flag_project(
env: Env,
oracle_signer: Address,
project_id: String,
reason: String,
signature: BytesN<64>,
nonce: u64,
)Stored in carbon_oracle contract storage:
// Key: MonitoringData(project_id, period)
MonitoringData {
project_id: String,
period: String,
tonnes_verified: i128,
methodology_score: u32,
satellite_cid: String,
submitted_by: Address,
submitted_at: u64, // ledger timestamp
}
// Key: LatestMonitoring(project_id) → period string (pointer to latest)
// Key: BenchmarkPrice(methodology, vintage_year) → price in stroops
// Key: OracleNonce → u64 (incremented per submission; prevents replay)| Store | Table / Object | Fields |
|---|---|---|
| IPFS | Satellite imagery bundle | GeoTIFF files, biomass density maps, change detection overlays |
| PostgreSQL | MonitoringData |
projectId, period, tonnesVerified, methodologyScore, satelliteCid, submittedBy, submittedAt |
| PostgreSQL | OracleUpdate |
idempotencyKey, type (monitoring/price), txHash, status, attempts, lastError |
| Error Code | Constant | Trigger |
|---|---|---|
| 8 | UnauthorizedOracle |
Caller is not the registered oracle signer |
| 22 | InvalidNonce |
Submitted nonce ≤ current stored nonce (replay attack prevention) |
| 23 | InvalidSignature |
Ed25519 signature verification fails |
| 13 | MonitoringDataStale |
is_monitoring_current() returns false — latest data is > 365 days old |
- Monitoring freshness gate —
is_monitoring_current(project_id)is called by the credit contract during minting. If it returns false, minting is blocked regardless of other conditions. - Nonce replay protection — Each oracle submission must include a nonce strictly greater than the stored nonce. The oracle service reads the current nonce before each submission.
- Oracle key rotation —
rotate_oracle(env, admin, new_oracle, new_pub_key)allows replacing the oracle keypair without redeploying the contract. Old submissions remain valid under the historical nonce chain. - Price deviation alert — The oracle service checks the previous benchmark price before submitting. If the new price differs by more than 15%, it sends an alert to
ADMIN_ALERT_WEBHOOKand waits for manual approval before posting. - Multiple monitoring periods — Each
(project_id, period)pair is stored independently. The oracle stores aLatestMonitoringpointer for fast freshness checks during minting.
Summary: The project developer requests issuance of a credit batch corresponding to verified tonnes. The contract assigns globally unique serial numbers to every credit in the batch, preventing double-counting at the mathematical level.
| Actor | Action |
|---|---|
| Project Developer | Submits mint request via dashboard with the amount to issue |
| Admin | Signs the mint_credits transaction |
Contract: carbon_credit
Function: mint_credits
mint_credits(
env: Env,
admin: Address,
project_id: String,
amount: i128, // tonnes to mint; max 1_000_000_000
vintage_year: u32,
batch_id: String, // globally unique batch identifier
serial_start: u64, // start of serial number range (inclusive)
serial_end: u64, // end of serial number range (inclusive)
metadata_cid: String, // IPFS CID for this batch's documentation
initial_owner: Address, // project developer's Stellar address
)The contract calls carbon_oracle.is_monitoring_current(project_id) before minting and calls verify_serial_range(serial_start, serial_end) to check the SerialRegistry for collisions.
// Key: Batch(batch_id)
CreditBatch {
batch_id: String,
project_id: String,
vintage_year: u32,
amount: i128,
serial_start: u64,
serial_end: u64,
issued_at: u64,
status: CreditStatus::Active,
metadata_cid: String,
owner: Address,
}
// Key: ProjectBatches(project_id) → Vec<String> (appended)
// Key: SerialRegistry → Vec<SerialRange> (appended; checked on every mint)Event emitted:
CreditMintedEvent {
batch_id, project_id, admin, amount,
vintage_year, serial_start, serial_end, timestamp
}| Store | Table / Object | Fields |
|---|---|---|
| PostgreSQL | CreditBatch |
id, batchId, projectId, vintageYear, amount, serialStart, serialEnd, status, metadataCid, issuedAt |
| IPFS | Batch documentation | Monitoring period reports, methodology calculation workbook, third-party audit reports |
| Error Code | Constant | Trigger |
|---|---|---|
| 2 | ProjectNotVerified |
Project status is not Verified |
| 3 | ProjectSuspended |
Project is under suspension |
| 6 | SerialNumberConflict |
Proposed [serial_start, serial_end] overlaps any range in SerialRegistry |
| 18 | InvalidSerialRange |
serial_start > serial_end or range exceeds amount |
| 16 | ZeroAmountNotAllowed |
amount is 0 |
| 13 | MonitoringDataStale |
Oracle freshness check fails (> 365 days) |
| 20 | MethodologyScoreLow |
Methodology score in latest monitoring data is < 70 |
- Serial number collision —
verify_serial_rangescans the entireSerialRegistrybefore minting. This is the core anti-double-counting mechanism. The backend pre-computes the next available serial range by querying the highestserial_endacross all existing batches. - Batch size cap — A single batch cannot exceed
MAX_BATCH_SIZE(1,000,000,000 credits). Large projects split issuance across multiple batches. - Partial issuance — Developers can mint less than the total verified tonnes (e.g., issue in tranches). Each tranche is a separate batch with non-overlapping serial ranges.
- Registry increment —
increment_issued(env, oracle_address, project_id, amount)is called oncarbon_registryafter a successful mint to tracktotal_credits_issuedagainsttotal_credits_retired.
Summary: A credit owner (typically the project developer) makes some or all of a batch available for purchase at a specified price per tonne. Listings are public and browsable without a wallet.
| Actor | Action |
|---|---|
| Credit Owner (Project Developer) | Creates a listing specifying amount and price |
| Oracle Service | price_oracle.py posts benchmark prices to carbon_oracle for reference |
Contract: carbon_marketplace
Function: list_credits
list_credits(
env: Env,
seller: Address,
listing_id: String,
batch_id: String,
project_id: String,
amount: i128,
price_per_credit_usdc: i128, // in stroops (1 USDC = 10_000_000)
vintage_year: u32,
methodology: String,
country: String,
)Sellers can remove an unsold listing with:
delist_credits(
env: Env,
seller: Address,
listing_id: String,
)// Key: Listing(listing_id)
MarketListing {
listing_id: String,
seller: Address,
batch_id: String,
project_id: String,
amount_available: i128,
price_per_credit: i128, // stroops
vintage_year: u32,
methodology: String,
country: String,
created_at: u64,
status: ListingStatus::Active,
}
// Key: AllListings → Vec<String> (appended)Event emitted:
ListingCreatedEvent {
listing_id, seller, batch_id, amount, price_per_credit, timestamp
}| Store | Table / Object | Fields |
|---|---|---|
| PostgreSQL | MarketListing |
id, listingId, projectId, batchId, seller, amountAvailable, pricePerCredit, vintageYear, methodology, country, status, createdAt |
| Error Code | Constant | Trigger |
|---|---|---|
| 3 | ProjectSuspended |
The project associated with the batch has been suspended |
| 4 | InsufficientCredits |
Seller does not own enough credits in the specified batch |
| 16 | ZeroAmountNotAllowed |
Listing amount is 0 |
| 12 | PriceNotSet |
No oracle benchmark price exists for this methodology + vintage (warning; listing can still proceed) |
- Partial listings — An owner can list a fraction of a batch. The remaining credits stay with the owner until listed or transferred.
- Re-listing — A seller can create multiple listings for the same batch as long as the total listed amount does not exceed their ownership.
- Suspended project delisting — If a project is suspended after listing,
suspend_project(env, admin, project_id)on the marketplace prevents new purchases against those listings. Existing sellers can still delist to recover their credits. - Price discovery — The frontend displays the oracle benchmark price alongside the seller's ask price to help buyers assess fair value.
Summary: A corporation buys credits from one or more listings. USDC is transferred from buyer to seller atomically in the same transaction as the credit transfer. A 1% protocol fee goes to the treasury.
| Actor | Action |
|---|---|
| Corporation (Buyer) | Selects listings, approves USDC spend, submits purchase transaction |
Contract: carbon_marketplace
// Single listing purchase
purchase_credits(
env: Env,
buyer: Address,
listing_id: String,
amount: i128,
)
// Multi-project bulk purchase (one transaction)
bulk_purchase(
env: Env,
buyer: Address,
listing_ids: Vec<String>, // max 10 listings per transaction
amounts: Vec<i128>, // parallel array; must match listing_ids length
)Internally, purchase_credits calls carbon_credit.transfer_credits(seller, buyer, batch_id, amount) and the USDC token contract to move payment.
The MarketListing.amount_available and MarketListing.status fields are updated:
| Condition | New status |
|---|---|
| All available credits purchased | ListingStatus::Sold |
| Partial purchase | ListingStatus::PartiallyFilled |
Credit ownership in carbon_credit is updated: CreditBatch.owner changes from seller to buyer for the purchased amount (partial ownership tracked off-chain; on-chain only tracks full batch ownership).
Event emitted:
PurchaseCompletedEvent {
listing_id, buyer, seller, amount, total_cost, timestamp
}| Store | Table / Object | Fields Updated |
|---|---|---|
| PostgreSQL | MarketListing |
amountAvailable, status, updatedAt |
| PostgreSQL | PurchaseRecord |
buyer, listingId, amount, totalCost, txHash, purchasedAt |
| Error Code | Constant | Trigger |
|---|---|---|
| 10 | ListingNotFound |
listing_id does not exist or has been delisted |
| 11 | InsufficientLiquidity |
Requested amount exceeds amount_available on the listing |
| 3 | ProjectSuspended |
The project was suspended after the listing was created |
| 4 | InsufficientCredits |
Seller no longer owns the credits (e.g., transferred out of band) |
- Bulk purchase limit —
bulk_purchaseaccepts a maximum ofMAX_BATCH_SIZE(10) listings per transaction to stay within Soroban transaction size limits. - Atomic execution — The entire purchase (USDC transfer + credit transfer + listing update) is a single Soroban transaction. If any step fails, the whole transaction reverts.
- Protocol fee — 1% of the purchase amount in USDC is sent to the
Treasuryaddress configured incarbon_marketplace. The seller receives 99% ofamount × price_per_credit. - Cart flow — The frontend accumulates listings into a BulkPurchaseCart and batches them into groups of ≤10 for submission. Multiple transactions are submitted sequentially if the cart exceeds 10 listings.
Summary: A corporation permanently removes credits from circulation, claiming the carbon offset. Retirement is cryptographically irreversible — no admin key can reverse it. The retired credits are burned on-chain with a permanent record of who retired them, on whose behalf, and why.
| Actor | Action |
|---|---|
| Corporation (Credit Owner) | Initiates retirement specifying beneficiary and reason |
| Backend | Generates certificate PDF and pins it to IPFS before the on-chain transaction |
Contract: carbon_credit
Function: retire_credits
retire_credits(
env: Env,
holder: Address,
batch_id: String,
amount: i128,
retirement_id: String, // globally unique; generated by backend
beneficiary: String, // legal entity name (e.g. "Acme Corp")
reason: String, // e.g. "2024 Scope 3 emissions offset"
certificate_cid: String, // IPFS CID of PDF certificate (pre-generated)
)This call is permanently irreversible. There is no unretire_credits function.
// Key: Retirement(retirement_id)
RetirementCertificate {
retirement_id: String,
credit_batch_id: String,
project_id: String,
amount: i128,
retired_by: Address,
beneficiary: String,
retirement_reason: String,
vintage_year: u32,
serial_numbers: Vec<u64>, // exact serials consumed
retired_at: u64,
tx_hash: String,
certificate_cid: String, // IPFS pointer to PDF
}CreditBatch.status is updated:
| Condition | New status |
|---|---|
| All credits in batch retired | CreditStatus::FullyRetired |
| Some credits retired | CreditStatus::PartiallyRetired |
Event emitted:
CreditRetiredEvent {
retirement_id, batch_id, project_id, amount,
retired_by, beneficiary, timestamp
}| Store | Table / Object | Fields |
|---|---|---|
| PostgreSQL | RetirementRecord |
id, retirementId, batchId, projectId, amount, vintageYear, retiredBy, beneficiary, retirementReason, serialStart, serialEnd, serialNumbers, txHash, certificateCid, isValid, retiredAt |
| IPFS | Retirement certificate PDF | Project name, methodology, vintage year, amount, serial numbers, beneficiary, reason, transaction hash, QR code, CarbonLedger branding |
| Error Code | Constant | Trigger |
|---|---|---|
| 4 | InsufficientCredits |
Holder does not own enough credits in the batch |
| 5 | AlreadyRetired |
The batch is already FullyRetired |
| 15 | RetirementIrreversible |
Attempted reversal of an existing retirement record (guard clause) |
| 16 | ZeroAmountNotAllowed |
amount is 0 |
| 3 | ProjectSuspended |
Project is suspended; retirement is still permitted but flagged |
- Certificate-first flow — The backend generates and pins the PDF certificate to IPFS before calling
retire_credits, so thecertificate_cidis embedded in the immutable on-chain record at the moment of retirement. - Partial retirement — A holder can retire part of a batch and keep the rest. Each partial retirement creates a new
RetirementCertificatewith its specific serial number range. - Suspended project retirement — Credits from a suspended project can still be retired. The certificate notes the project's suspension status. New purchases are blocked; retirement is not.
- Duplicate retirement_id — The backend generates UUIDs for
retirement_id. If the same UUID is submitted twice, the second call returnsAlreadyRetired. - Serial number traceability — The
serial_numbersarray inRetirementCertificatecontains the exact range of serial numbers retired. Anyone can look up a serial number in the audit explorer and trace it back to the original project, batch, and eventual retirement.
Summary: A permanent, publicly verifiable certificate is generated and made available at a stable URL. No wallet is required to view or verify it. The certificate is the final artefact that a corporation includes in ESG disclosures, regulatory filings, or sustainability reports.
| Actor | Action |
|---|---|
| Backend | Generates PDF, pins to IPFS, stores public URL in database |
| IPFS (Pinata) | Pins certificate for permanence |
| Public / Auditor | Verifies certificate via public URL or QR code scan |
Contract: carbon_credit
Function: get_retirement_certificate
get_retirement_certificate(
env: Env,
retirement_id: String,
) -> RetirementCertificateThis is a read-only query used by the verification page to display on-chain data alongside the PDF.
The RetirementCertificate stored in Stage 7 is immutable and permanent. No new data is written in Stage 8.
| Store | Object | Contents |
|---|---|---|
| IPFS | Certificate PDF | All retirement details, QR code pointing to public verification URL, CarbonLedger signature |
| PostgreSQL | RetirementRecord.certificateCid |
IPFS CID for permanent retrieval |
| Field | Source |
|---|---|
| Retirement ID | Generated by backend (UUID) |
| Transaction hash | Stellar ledger |
| Credits retired (amount + serial numbers) | RetirementCertificate.serial_numbers |
| Beneficiary | Submitted by retiring corporation |
| Retirement reason | Submitted by retiring corporation |
| Project name and methodology | CarbonProject in registry |
| Vintage year | CreditBatch.vintage_year |
| Country of origin | CarbonProject.country |
| Retirement date | RetirementCertificate.retired_at |
| QR code | Links to https://app.carbonledger.com/retire/[retirement_id] |
| IPFS CID | For decentralised permanent access |
Anyone can verify a retirement without a wallet:
- URL —
https://app.carbonledger.com/retire/[retirement_id]renders the on-chain certificate and project details. - QR code — Embossed on the PDF; resolves to the same public URL.
- Serial number lookup —
https://app.carbonledger.com/auditaccepts a serial number and returns the full provenance chain: minting batch → listing → purchase → retirement. - Bulk export —
GET /api/v1/retirements/export/csvreturns all retirements for an authenticated corporation in CSV format for ESG report integration.
- IPFS pinning failure — If Pinata fails to pin the certificate, the retirement transaction is not submitted. The backend retries pinning via the BullMQ queue before proceeding.
- Certificate regeneration — If the PDF needs to be re-generated (e.g., branded update), the new PDF gets a new CID. The original on-chain
certificate_cidis not changed; both CIDs resolve to valid certificates. - Permanent URL guarantee — The public verification URL is keyed on
retirement_id, which is immutable once the retirement transaction is confirmed. The URL will remain valid indefinitely.
Complete table of CarbonError codes used across all contracts:
| Code | Constant | Contract(s) | Meaning |
|---|---|---|---|
| 1 | ProjectNotFound |
registry, marketplace | project_id not found in storage |
| 2 | ProjectNotVerified |
credit | Project status is not Verified |
| 3 | ProjectSuspended |
registry, credit, marketplace | Project is Suspended; issuance / listing blocked |
| 4 | InsufficientCredits |
credit, marketplace | Holder or seller does not own enough credits |
| 5 | AlreadyRetired |
credit | Batch is FullyRetired |
| 6 | SerialNumberConflict |
credit | Proposed serial range overlaps an existing one |
| 7 | UnauthorizedVerifier |
registry | Caller is not on the verifier allow-list |
| 8 | UnauthorizedOracle |
oracle | Caller is not the registered oracle signer |
| 9 | InvalidVintageYear |
credit | Vintage year is in the future or before 1990 |
| 10 | ListingNotFound |
marketplace | listing_id not found or delisted |
| 11 | InsufficientLiquidity |
marketplace | Requested amount > amount_available on listing |
| 12 | PriceNotSet |
marketplace | No oracle benchmark price for this methodology + vintage |
| 13 | MonitoringDataStale |
credit, oracle | Latest monitoring data is > 365 days old |
| 14 | DoubleCountingDetected |
credit | Reserved; raised if serial registry is inconsistent |
| 15 | RetirementIrreversible |
credit | Guard clause; retirement record cannot be reversed |
| 16 | ZeroAmountNotAllowed |
credit, marketplace | Amount parameter is 0 |
| 17 | ProjectAlreadyExists |
registry | project_id already registered |
| 18 | InvalidSerialRange |
credit | serial_start > serial_end or range does not match amount |
| 19 | AlreadyInitialized |
all | Contract initialize() called more than once |
| 20 | MethodologyScoreLow |
registry | Score < 70; project cannot advance to Verified |
| 21 | UnauthorizedUpgrade |
all | Caller is not Admin when calling upgrade() |
| 22 | InvalidNonce |
oracle | Nonce ≤ stored nonce; replay attack prevention |
| 23 | InvalidSignature |
oracle | Ed25519 signature verification failed |