Confirm with HeartBank®: reference
A person's own passkey press for one action your app names: a human in the loop, so you know a person, not a script, did it.
Read from the live code: login.333.eco (src/confirm.ts, the request and the redeem) and sign.333.eco (/confirm, the page where the person presses). The press made into a product: the one thing an agent can do nothing about.
The flow: (1) your server asks POST /confirm/request with ITS OWN sub for the person (from Log in with HeartBank®), a kind, the action's SHA-256 and one of your registered redirect URLs; login answers with a ticket. (2) Send the person's browser to https://sign.333.eco/confirm#ticket=…&state=…; the page shows your app's REGISTERED name and what is being confirmed, and asks for that person's own passkey, with the device's own check. (3) The browser comes back to your page with #confirm=<result>&state=…, or #declined=1&state=…. (4) Your server redeems the result at POST /confirm/redeem: once, for your app only, and only if the passkey pressed is that person's.
⚠️ What it proves, and no more: the holder of this person's passkey pressed it, with their device's own check, for this action, just now. That is presence, not personhood: never present a confirmed person as "verified" (a PIN can satisfy the check, and a passkey can be shared or synced).
⛔ A closed list of kinds (today: upload): your app never puts its own words on sign.333.eco, so the page can't be turned into a form that says something else. Nothing is kept: no public record and no history line. For a file the person should put their name to, in public and for good, use Sign with HeartBank® instead.
Any registered app may use it, for a person who has logged in to it with Log in with HeartBank® (a person who last logged in before 2026-11-11 is found from their next log-in).
POST https://login.333.eco/confirm/request
Step 1: ask a person to confirm one action
Who may call it: Sent as FORM fields, not a header: client_assertion_type = urn:ietf:params:oauth:client-assertion-type:jwt-bearer and client_assertion = an ES256 JWT, iss = sub = your client id, aud = the endpoint's own address, one-use jti. The same keys as your Log in with HeartBank® client.
Body application/x-www-form-urlencoded
| Field | Type | What it is |
|---|
client_assertion_type | string | |
client_assertion | string | |
sub | string | Your app's own sub for the person, from their Log in with HeartBank® ID token. |
kind | "upload" | What they are confirming, from a closed list: the page says it in its own words. |
hash optional | string | Optional: the SHA-256 of the thing (a file being uploaded), 64 lowercase hex. The page shows its first eight. |
return_to | string | One of your app's registered redirect URLs. |
Answers
200 The ticket. { ticket, url, expires_in }400 bad_request: a kind outside the list, a malformed hash, or a return_to your app never registered. { error, why }401 invalid_client: the client assertion was refused. { error, why }404 not_found: the one answer for a sub nobody holds, a person who hasn't logged in to your app since 2026-11-11, and one with no passkey known. { error, why }
What a success answer holds
| Field | Type | What it is |
|---|
ticket | string | Opaque to your app; put it in the page's address fragment, never a query. |
url | string | https://sign.333.eco/confirm |
expires_in | integer | Seconds the ticket lasts (600). |
On the MCP server: not a tool: needs the app's own private key on its server; ⛔ the MCP server never holds or passes a developer's key
GET https://sign.333.eco/confirm
Step 2: the page where the person presses
Put the ticket and your state in the FRAGMENT: https://sign.333.eco/confirm#ticket=…&state=…. The passkey sheet opens on arrival. The browser returns to your return_to with #confirm=<result>&state=…, or #declined=1&state=…. ⛔ Never time them or nudge them on your side while they decide.
Who may call it: anyone, no key
Answers
200 The confirm page (HTML).
On the MCP server: not a tool: a browser step: the person's own press
POST https://login.333.eco/confirm/redeem
Step 3: redeem the result, once
Then check sub is the person you asked about and hash is the file you hold, before you accept the action.
Who may call it: Sent as FORM fields, not a header: client_assertion_type = urn:ietf:params:oauth:client-assertion-type:jwt-bearer and client_assertion = an ES256 JWT, iss = sub = your client id, aud = the endpoint's own address, one-use jti. The same keys as your Log in with HeartBank® client.
Body application/x-www-form-urlencoded
| Field | Type | What it is |
|---|
client_assertion_type | string | |
client_assertion | string | |
result | string | The confirm value the browser brought back. |
Answers
200 Confirmed. { confirmed, sub, kind, hash, at }400 bad_result (not sign.333.eco's, wrong audience, missing claims, too old) or replayed (already redeemed). { error, why }401 invalid_client. { error, why }403 not_yours (made for another app) or not_this_person (the passkey pressed is not the person you asked about). { error, why }
What a success answer holds
| Field | Type | What it is |
|---|
confirmed | true | |
sub | string | Your app's own sub for the person who pressed: check it is the person you asked about. |
kind | "upload" | |
hash optional | string | The SHA-256 you asked about: check it is the file you hold. |
at | integer | When they pressed (seconds since 1970). |
On the MCP server: not a tool: needs the app's own private key on its server; ⛔ the MCP server never holds or passes a developer's key