Thank with HeartBank®: reference
Record a thank on the gratitude ledger. Your app prepares it, the person presses their own passkey, your app commits it.
The gratitude ledger's API (repo H3/heartbank.net/ledger.heartbank.net, src/http.ts). ⭐ An agent can do everything except the press: every thank is signed by the person's own passkey at sign.333.eco, so no app and no agent can write a thank alone.
⚠️ First-party apps only today. Outside apps write from season 2 (7 January 2027), after review, and then Kiitos, and Kiitti only as a person's thanks to all of life (directive #94).
⚠️ A refusal says nothing: when the ledger declines a thank, the answer is only {"written": false}, with no reason and no count. It protects the people involved; it is not a bug.
The flow: (1) POST /v1/prepare → {pending, ticket}. (2) Send the person's browser to https://sign.333.eco/press#ticket=…&return_to=…&state=…; the page shows them what they are signing, and they press. The browser comes back to your return_to with #press=…&state=…. (3) Your server exchanges that result at POST https://login.333.eco/press/wrap (form field result) for a pressToken. (4) POST /v1/commit → the receipt's id.
POST https://ledger.heartbank.net/v1/prepare
Step 1: prepare a thank
Runs the rule. If it would write, builds the thank and returns a ticket for the person's press. At most 6 prepares a minute per person.
Who may call it: Your app's server proves itself with a fresh ES256 JWT (private_key_jwt): iss = sub = your client id, aud = https://ledger.heartbank.net, at most 5 minutes old, with a jti never used before. ⚠️ Its own header, not Authorization: Cloud Run reads Authorization: Bearer as a Google token. ⛔ Never sent from a browser or a phone.
Body application/json
One of: { ref, pidToken, sub, ledger, to, cargo }
| Field | Type | What it is |
|---|
ref | string | Your id for this thank. The same ref returns the same answer and writes once. |
pidToken | string | |
sub | string | |
ledger | "kiitos" | |
to | { pidToken, sub } | ⏳ Outside apps will name the receiver by registry handle instead, never another person's token (A591 (3)). |
cargo optional | string | Optional: exactly 32 bytes of your own, committed with a salt (never stored as sent); it reaches the two people in their records. |
Or: { ref, pidToken, sub, ledger, dir, evidence }
First-party only (B-Twin, thank.333.eco): care for a thing, plant or animal, decided by the ledger's public rule on witnessed evidence. ⛔ Never an outside app: from season 2 an outside app's Kiitti is only a person's thanks to all of life, a call with no direction, twin or evidence field (directive #94).
| Field | Type | What it is |
|---|
ref | string | |
pidToken | string | |
sub | string | |
ledger | "kiitti" | |
dir | "kind" | "thankful" | |
evidence | { kind, twinId, attested, match } | |
Answers
200 A ticket, or not written. object or object400 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }403 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }503 The ledger is unavailable; try again. { error }
On the MCP server: not a tool: ⛔ no tool can write a real thank (SPEC §6.1)
POST https://ledger.heartbank.net/v1/commit
Step 3: commit the pressed thank
The ledger checks the press itself, against exactly what its zero-sum proof can hold. Safe to retry: the answer is kept under ref.
Who may call it: Your app's server proves itself with a fresh ES256 JWT (private_key_jwt): iss = sub = your client id, aud = https://ledger.heartbank.net, at most 5 minutes old, with a jti never used before. ⚠️ Its own header, not Authorization: Cloud Run reads Authorization: Bearer as a Google token. ⛔ Never sent from a browser or a phone.
Body application/json
| Field | Type | What it is |
|---|
ref | string | |
pending | string | |
pressToken | string | From POST https://login.333.eco/press/wrap. |
Answers
200 Written, or not. object or object400 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }403 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }503 The ledger is unavailable; try again. { error }
On the MCP server: not a tool: ⛔ no tool can write a real thank, and none can press (SPEC §6.1)
POST https://ledger.heartbank.net/v1/place
Place one side of a written thank on the public map
A cell number and a one-use token, and nothing else: there is no field for a coordinate. Call it later than the thank, so a placing's time does not point at a thank.
Who may call it: Your app's server proves itself with a fresh ES256 JWT (private_key_jwt): iss = sub = your client id, aud = https://ledger.heartbank.net, at most 5 minutes old, with a jti never used before. ⚠️ Its own header, not Authorization: Cloud Run reads Authorization: Bearer as a Google token. ⛔ Never sent from a browser or a phone.
Body application/json
| Field | Type | What it is |
|---|
token | string | |
cell | integer | An area about 100 km across (0.9° bands), from the ledger's open cell function. |
Answers
200 Placed, or not. { placed }400 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }403 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }503 The ledger is unavailable; try again. { error }
What a success answer holds
| Field | Type | What it is |
|---|
placed | boolean | |
On the MCP server: not a tool: ⛔ no tool can write a real thank (SPEC §6.1)
GET https://ledger.heartbank.net/v1/aura
Read a person's ring
A color and a width per ledger this app may read, for the logged-in person. ⛔ No balance, no count: a ring, never a number to chase.
Who may call it: Your app's server proves itself with a fresh ES256 JWT (private_key_jwt): iss = sub = your client id, aud = https://ledger.heartbank.net, at most 5 minutes old, with a jti never used before. ⚠️ Its own header, not Authorization: Cloud Run reads Authorization: Bearer as a Google token. ⛔ Never sent from a browser or a phone.
Parameters
| Name | Type | What it is |
|---|
sub (query) | string | |
x-pid-token (header) | string | |
Answers
200 The rings this app may read. { kiitos, kiitti }403 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }503 The ledger is unavailable; try again. { error }
What a success answer holds
| Field | Type | What it is |
|---|
kiitos optional | { color, ring } | |
kiitti optional | { color, ring } | |
On the MCP server: not a tool: needs a person's token; ⛔ an agent never holds one
POST https://ledger.heartbank.net/v1/submit
Retired: a thank without a press (retired)
The test tree's season 0 only. From season 1 every thank needs the person's press, and this answers 403 (founder 2026-11-09).
Who may call it: Your app's server proves itself with a fresh ES256 JWT (private_key_jwt): iss = sub = your client id, aud = https://ledger.heartbank.net, at most 5 minutes old, with a jti never used before. ⚠️ Its own header, not Authorization: Cloud Run reads Authorization: Bearer as a Google token. ⛔ Never sent from a browser or a phone.
Answers
403 Refused (a bad assertion, an app not admitted, an app whose writing is paused, a malformed body). { error }
On the MCP server: not a tool: retired