The Borneo Sawit API and MCP server: daily TBS prices, no API key
Every price this site publishes is also available as JSON, open to anyone, with no key and no sign-up. This page explains what the data means before it explains how to fetch it, because the mistake people make with it is not an HTTP mistake, it is a misreading of what a number is.
What you are actually reading
One row is one price, for one date, one mill or one loading ram, and one reporter. Not an average, and not an index.
Two reporters can quote different prices for the same mill on the same day. Both are published side by side rather than averaged, because the gap between them is itself the information.
Dates are WITA calendar dates (UTC+8), not instants. A price reported at seven in the morning belongs to that day, not to the previous one that UTC would put it on.
Corrections are appended as new rows rather than overwriting old ones. What you read is always the live revision.
A PKS gate price and a RAM price to farmers are two different claims and must never be averaged together. One is what a mill pays at its gate, the other is what a collector pays a farmer at its yard, and the difference between them is the collector’s margin.
The `change` field is null when a pair has never reported before, and 0 when the price did not move. Those are different facts; never read null as zero.
The coverage is South Kalimantan. This is not CPO, not a national index, and not the Dinas Perkebunan gazette, though the latest Disbun partnership reference figure rides along on each board for comparison.
Start here
There is no API key for reads. No sign-up, no token, and no header you have to send. Copy the line below and you have today’s board.
curl -s https://borneosawit.com/api/v1/prices/latest
The answer carries the date it is actually describing. When today has nothing on it yet you get the most recent day that does, stamped with its own date, rather than an empty array. CORS is open to every origin, so a browser `fetch` works too. The base URL is `https://borneosawit.com/api/v1`.
The endpoints
Nine of them, all GET, all JSON, all anonymous. Every failure uses the same envelope, `{ "error": { "code", "message", "hint", "docs" } }`, and the field to branch on is `code`, never `message`.
- /api/v1/prices/latest
- The freshest board: today in WITA, or the most recent day that has one.
- /api/v1/prices/{date}
- The board for one exact date. An empty day is still a 200 with empty arrays.
- /api/v1/prices/history
- One mill-and-reporter pair over consecutive days. `millId` and `reporterId` are both required.
- /api/v1/mills
- Every mill, with every written form of its name.
- /api/v1/rams
- Every loading ram, with its area and its aliases.
- /api/v1/reporters
- Everyone who announces a price: mills publishing their own, and collectors reporting.
- /api/v1/articles
- The articles, without their bodies. `?locale=` for one language.
- /api/v1/articles/{slug}
- One article in full, its body as a list of blocks.
- /api/v1/status
- Whether the database is answering, and the newest price date. Never cached.
MCP: connect it straight to Claude
The same data is also an anonymous MCP server with seven tools. No login, no OAuth, and no token to paste anywhere.
claude mcp add --transport http borneo-sawit https://borneosawit.com/mcp
In Claude Code, run the line above. In the Claude apps, add it as a custom connector with the URL `https://borneosawit.com/mcp` and leave the authentication section empty.
The transport is Streamable HTTP over POST, stateless, with no session id and no SSE. A GET is answered 405, which is the right answer for a server that never initiates a message.
The tools are `get_price_board`, `get_price_history`, `list_mills`, `list_rams`, `list_reporters`, `list_articles` and `get_article`. None of them writes anything.
The machine-readable documents
Beyond the endpoints above, every page on this site also answers `Accept: text/markdown` with a markdown rendition of itself.
- /openapi.json
The OpenAPI 3.1 spec for everything above
- /.well-known/mcp.json
The MCP manifest, in the registry’s own server.json shape
- /llms.txt
A summary of the site for language models
- /sitemap.xml
Every URL we ask to have indexed, in three languages
If you are the one reporting prices
Writing prices is a separate, closed surface. It is at `https://borneosawit.com/api/mcp`, it speaks OAuth 2.1 with PKCE, and dynamic client registration is open at `https://borneosawit.com/api/oauth/register` so Claude can register itself. What is not open is the account: only a registered reporter can approve a connection, and nothing on this page can change a single figure. If you are a mill or a collector and you want your prices published here, message us on WhatsApp.
The CLI
There is a zero-dependency Node CLI in the `cli/` folder of this repository. It is not published to npm yet, and this page will say so the day it is. For now, run it from a checkout, or use `curl` as above.
node cli/bin/borneo-sawit.mjs prices
The words that appear inside the data
The field names are English, but the values are not translated. These six appear verbatim inside a response, and are deliberately left as they are.
- TBS
- Tandan Buah Segar, the fresh fruit bunch a farmer sells by the kilo. The only thing priced here.
- PKS
- Pabrik Kelapa Sawit, the palm oil mill. It buys fruit at its own gate.
- RAM
- A loading ram: a collector’s yard that buys fruit straight from farmers and hauls it to a mill. Its price is a farmgate price, never a mill-gate one.
- peron
- A collector. One word for two roles this data keeps apart: a peron reporting mill prices, and a RAM setting its own price to farmers. The same yard is often both.
- Disbun
- Dinas Perkebunan, the provincial plantation office. It sets a periodic partnership reference price; the latest figure rides along on each board for comparison.
- kabupaten
- An Indonesian regency. The names are never translated, because a name is also the string things are matched against.