# kotobase — full reference (for LLMs/agents) kotobase (https://kotobase.net) is a Knowledge Graph BaaS and content-addressed graph hosting service built on kotoba (https://github.com/etzhayyim/kotoba), a Datomic-over-IPFS distributed knowledge graph. It serves engineers and enterprises that need managed graph databases, KG ingest, Datalog, SPARQL/Cypher- style query, MCP access, and durable provenance. A pin = a named commit CID in kotoba's quad store; once pinned a CID is durable (off-site via CAR-on-B2) and addressable over IPFS. Identity: did:web:kotobase.net. Legacy alias host: kotobase.gftd.ai. ## Authentication Tenant calls accept either `Authorization: Bearer `, where the gftd auth JWT `sub` is the tenant DID, or `Authorization: CACAO ` with `x-kotoba-did: `. The CACAO must grant `kotobase:pin` over the tenant DID as graph scope; the pod verifies the signature and issuer binding. ## A. IPFS Pinning Service API (standard) Spec: https://ipfs.github.io/pinning-services-api-spec/ . Base: https://kotobase.net (clients append /pins). Native tooling: ipfs pin remote service add kotobase https://kotobase.net ipfs pin remote add --service=kotobase --name= ipfs pin remote ls --service=kotobase ipfs pin remote rm --service=kotobase --cid= Endpoints: - POST /pins body {cid, name?} -> 202 PinStatus - GET /pins?cid&name&status&limit -> {count, results:[PinStatus]} - GET /pins/{requestid} -> PinStatus | 404 - POST /pins/{requestid} body {cid, name?} -> replace (delete+create) - DELETE /pins/{requestid} -> 202 PinStatus = {requestid, status:queued|pinning|pinned|failed, created, pin:{cid,name,origins,meta}, delegates, info}. Errors = {error:{reason, details?}} with appropriate HTTP status. `requestid` is stable across create/list/delete (so rm-by-cid = list->delete works). ## B. XRPC API POST https://kotobase.net/xrpc/ai.gftd.apps.kotobase. , JSON body, Bearer JWT or CACAO. Pinning & account: - pinCreate {name, cid?|quads?, size_hint_bytes?} -> {ok, pin_id, cid, status, size_bytes} - pinList {status?, limit?, offset?} -> {ok, pins:[{pin_id, name, cid, status, size_bytes, created_at}], total, offset, limit} - pinDelete {pin_id} -> {ok} - accountCreate {} ; accountStatus {} -> {ok, tenantDid, tier, quotaPins, quotaBytes, usedPins, usedBytes, storage_mode?} - usageGet {} -> {ok, tenantDid, pinCount, totalBytes, quotaPins, quotaBytes, storage_mode?} Worker-B2 keeps snake_case compatibility fields such as quota_pins, quota_bytes, used_pins, and used_bytes for older account/usage clients. Query (read; tenant JWT): - datomic.q {graph, query_edn, inputs_edn?, as_of?, since?, history?} -> {graph, rows_edn, ...} - datomic.pull / pullMany / datoms / entity / asOf / since / history / tx / txRange / log / basisT / dbStats / seekDatoms / indexRange / indexPull / sync - graph.sparql {query, graph?, limit?} ; graph.query (SPARQL/Cypher over the Datom head) - kg.query {lang:"sparql"|"cypher"|"sql", query, limit?} - web.search {q, db_name?|graph?, limit?} -> {ok, count, results:[{url, title, snippet, score}]} (webpage keyword search: one capped datomic.q Datalog scan + edge-side ranking; ASCII + Japanese/CJK-bigram tokens) - geo.search {bbox|near:{lat,lon,radius_m}, q?, db_name?|graph?, limit?} -> {ok, count, results:[{id, name, lat, lon, distance_m?}]} (maps/place search: one capped datomic.q scan + edge-side bbox/haversine filtering and distance ranking) Write (tenant; JWT sub == tenant_did or CACAO issuer == tenant_did): - kg.ingest {id, type?, label_ja?, label_en?, claims?, relations?, ...} - kg.ingest_batch {entities:[...]} - web.ingest {url, title?, text?, lang?, db_name?, cacao_b64} -> datomic.transact passthrough (caller-extracted page -> :webpage/* datoms; same tenant-cap CACAO gate as datomic.transact; the edge does not crawl) - geo.ingest {name, lat, lon, id?, tags?, db_name?, cacao_b64} -> datomic.transact passthrough (place -> :place/* datoms; id defaults to a deterministic slug of name) - datomic.transact / with are the tenant-capability Datom write plane when enabled; the edge derives the tenant graph from db_name and the caller DID so a raw client-supplied graph cannot override ownership. Generic /xrpc/:nsid allowlist (exact): Read/query: - `ai.gftd.apps.kotobase.datomic.q` - `ai.gftd.apps.kotobase.datomic.pull` - `ai.gftd.apps.kotobase.datomic.pullMany` - `ai.gftd.apps.kotobase.datomic.datoms` - `ai.gftd.apps.kotobase.datomic.seekDatoms` - `ai.gftd.apps.kotobase.datomic.indexRange` - `ai.gftd.apps.kotobase.datomic.indexPull` - `ai.gftd.apps.kotobase.datomic.entity` - `ai.gftd.apps.kotobase.datomic.ident` - `ai.gftd.apps.kotobase.datomic.entid` - `ai.gftd.apps.kotobase.datomic.asOf` - `ai.gftd.apps.kotobase.datomic.since` - `ai.gftd.apps.kotobase.datomic.history` - `ai.gftd.apps.kotobase.datomic.tx` - `ai.gftd.apps.kotobase.datomic.txRange` - `ai.gftd.apps.kotobase.datomic.log` - `ai.gftd.apps.kotobase.datomic.basisT` - `ai.gftd.apps.kotobase.datomic.dbStats` - `ai.gftd.apps.kotobase.datomic.sync` - `ai.gftd.apps.kotobase.graph.sparql` - `ai.gftd.apps.kotobase.graph.query` - `ai.gftd.apps.kotobase.kg.query` Tenant write: - `ai.gftd.apps.kotobase.kg.ingest` - `ai.gftd.apps.kotobase.kg.ingest_batch` Example: curl -s -X POST https://kotobase.net/xrpc/ai.gftd.apps.kotobase.pinCreate \ -H "authorization: Bearer $JWT" -H 'content-type: application/json' \ -d '{"name":"my-doc","cid":"bafyrei..."}' ## C. MCP (AI agents) https://mcp.gftd.ai/mcp — Model Context Protocol facade; tools are generated from the same ai.gftd.apps.kotobase.* lexicons (pin, account, query, ingest). ## D. Retrieval Pinned content is content-addressed; resolve by CID at https://ipfs.gftd.ai/ipfs/ or via any IPFS gateway/node. ## E. Durability (CAR-on-B2) Local durable tier: kubo (IPFS) blockstore on the pod PVC (KOTOBA_IPNS=kubo). Off-site: each commit's blocks are packed into ONE CAR object archived to Backblaze B2 (bucket ai-gftd-nats, key = commit CID). Object count is proportional to commits, not blocks, so it scales where a per-block S3 datastore does not. Cold reads are served from B2 via a single ranged GET against the CAR, located by a local index. See ADR-2606042100 (CAR-on-B2) and ADR-2606042200 (this PSA). Worker-B2 archive mode: operators can set KOTOBASE_STORAGE_MODE=b2 to serve the pin control plane from Cloudflare Worker + Backblaze B2 metadata without the Kubernetes pod. With KOTOBASE_IPFS_GATEWAY_URL it can archive gateway-produced IPFS/IPNS IPLD CARs and serve them from /ipfs/* and /ipns/*. This is an archive mode, not an IPFS swarm node; production currently remains in backend/Kubo mode and Worker-B2 mode verifies did:key EdDSA CACAO itself. Quota-safe metadata writes use a Durable Object tenant lock. EIP-191 / smart-account CACAO remains backend-only. See ADR-2606110003. ## F. Quotas, tiers & business model accountStatus returns tier + quotas. Free tier is 100 MB / 3 pins for developer validation. Standard targets self-serve engineers with 50 GiB / 1,000 pins. Pro targets production teams with 500 GiB / 10,000 pins. Regulated is a contract tier for dedicated graph infrastructure, CACAO-only posture, read audit, key custody, and compliance review. Worker-B2 also keeps quota_pins / quota_bytes compatibility fields for older clients. ## G. Roadmap - sk_live_* API keys for non-interactive clients (today: gftd-AUTHN JWT or CACAO). - PinStatus.delegates (swarm multiaddrs) to push blocks for a not-yet-public CID over bitswap.