From 2d1a8cf5cce8b6f8bb9776c7f54032952e4fed1d Mon Sep 17 00:00:00 2001 From: chrisjleal <184772742+chrisjleal@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:08:53 -0600 Subject: [PATCH 1/3] Talk to your agent from any app: your app's users can talk to it The sign-in with ProofableSignIn, the call as the person with agentWallet, what the agent may do for them, and the public API spec with the agentWallet description. --- CHANGELOG.md | 2 ++ agents/talk-to-an-agent.mdx | 28 +++++++++++++++++++++++++++- openapi/public-api.json | 4 ++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c121533..dbfcb9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Product release notes: [docs.proofable.me/changelog](https://docs.proofable.me/c ### Added +- **Your app's users can talk to your agent.** [Talk to your agent from any app](/agents/talk-to-an-agent) shows the sign-in with `ProofableSignIn` and the call as the person: `agentChat` with `agentWallet` runs your public agent as a template in their own profile, on their records and credits, with nothing of yours in the conversation. + - **Benchmark.** [Benchmark](/benchmark) replaces the Evidence page. It opens on one table of outcomes, each beside the setup it was measured against: 48 of 48 governance scenarios, every out-of-permission action stopped before it runs, 0 of 6 data-theft attempts out of a hijacked agent, 150 of 150 forged records caught, $0.024 against $0.078 per handed-off result, reuse 750 times faster than redoing the work, 42 times fewer tokens for agent memory, a 3.1 times lower worst-case research spend, and one identity check reused by 100 apps for $1.75 against $150. How each result was measured is in one section for reviewers. `/evidence`, `/benchmarks` and `/use-cases/authority-at-dispatch` open it. - **Sell a service your server runs.** [Your server runs it](/gates/your-server) covers a listing whose orders go to your own URL with the buyer's inputs and repository, and whose result you post back onto the buyer's receipt. diff --git a/agents/talk-to-an-agent.mdx b/agents/talk-to-an-agent.mdx index 1da072a..8d50672 100644 --- a/agents/talk-to-an-agent.mdx +++ b/agents/talk-to-an-agent.mdx @@ -29,6 +29,32 @@ turn.credits; // what the turn spent Sign the client in with an [access key](/deployment/access-keys) on a server, or with the signed-in person's session in a browser. Never ship an access key in browser code. +## Let your app's users talk to your agent + +Sign your users in with Proofable, then make the same call as them. Each person talks to your public agent inside their own Proofable profile: their records, their credits, and nothing of yours in the conversation. What the agent knows is its instructions and the records you made public. + +```javascript +import { ProofableClient, ProofableSignIn } from '@proofable/sdk'; + +// 1. Sign-in. Register your callback once and keep the client id. +const signIn = new ProofableSignIn({ redirectUri: 'https://your.app/proofable/callback', clientId }); +const { url, state, verifier } = await signIn.start(); // send the person to url; keep state and verifier +const tokens = await signIn.finish({ code, verifier, state, expectedState }); // when they are back + +// 2. Act as the person. +const person = new ProofableClient({ apiKey: tokens.accessToken }); +const who = await person.me(); // { handle, address, ... } +const turn = await person.agentChat({ + agentId: 'my-agent', + agentWallet: '0xYourAccount', // your account: the agent runs as a template in theirs + messages: [{ role: 'user', content: 'Where does the case start?' }], +}); +``` + +What the agent may do for the person is fixed: read and add to the person's own records, and search the web. It cannot spend, grant, change keys or run code, and a high-risk action waits for the person. When it keeps words the person confirms, the record is the person's own, and they can open it at `proofable.me/proof/`. + +The sign-in is the one every MCP host uses ([MCP OAuth](/mcp/oauth)). `start()` with no `clientId` registers your callback and returns the id; store it with your app's settings. Renew with `signIn.refresh(tokens.refreshToken)` before the token expires. + ## What happens on each turn 1. Proofable finds the [permission](/agents/agent-delegation) you signed for the agent. No active permission from your account, no turn. @@ -50,7 +76,7 @@ curl -N https://api.proofable.me/api/v1/agent/chat/turn \ | Field | What it does | |---|---| | `agentId` | Run the turn as this agent. Omit it to run as yourself. | -| `agentWallet` | The agent's account, when two of your agents share an id | +| `agentWallet` | The agent's account: when two of your agents share an id, or to talk to someone else's public agent, which then runs as a template in your own profile | | `messages` | The conversation, `user` and `assistant` turns, ending with the user | | `systemPrompt` | Context from your app. The agent reads it as data, after its own instructions. | | `model` | `default` or `advanced`. With `provider`, that provider's model id. | diff --git a/openapi/public-api.json b/openapi/public-api.json index f3aaf46..330d2d3 100644 --- a/openapi/public-api.json +++ b/openapi/public-api.json @@ -4838,7 +4838,7 @@ "post": { "operationId": "streamAgentChatTurn", "summary": "Talk to an agent: run one chat turn as it, streamed as Server-Sent Events", - "description": "Runs one turn on the hosted agent runtime. With agentId the turn runs as that agent: its instructions and skills, under the delegation you signed, every tool call checked against it. The final event carries the answer, the records it read as citations, and the credits it spent. Nothing to deploy: call it from any app or runtime with a session, an OAuth token or a profile access key.", + "description": "Runs one turn on the hosted agent runtime. With agentId the turn runs as that agent: its instructions and skills, under the delegation you signed, every tool call checked against it. Name agentWallet to talk to someone else's public agent: it then runs as a template in your own profile, on your records and credits, and nothing of its owner's is part of the conversation. The final event carries the answer, the records it read as citations, and the credits it spent. Nothing to deploy: call it from any app or runtime with a session, an OAuth token or a profile access key.", "tags": [ "Agents" ], @@ -4859,7 +4859,7 @@ }, "agentWallet": { "type": "string", - "description": "The agent's account, when two of the caller's agents share an agentId." + "description": "The agent's account: when two of the caller's agents share an agentId, or to talk to someone else's public agent, which then runs as a template in the caller's own profile." }, "messages": { "type": "array", From 40af9423ec802cd11f6ab0c4d8eafd143bb18d3e Mon Sep 17 00:00:00 2001 From: chrisjleal <184772742+chrisjleal@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:13:41 -0600 Subject: [PATCH 2/3] Talk to your agent from any app: your app's users talk to it, and your app pays The sign-in with ProofableSignIn, the call as the person with agentWallet, who pays with appId and billingWallet, what the agent may do for them, and the refusal table with SPONSOR_CAP_EXCEEDED. --- CHANGELOG.md | 2 +- agents/talk-to-an-agent.mdx | 39 +++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbfcb9c..5157316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Product release notes: [docs.proofable.me/changelog](https://docs.proofable.me/c ### Added -- **Your app's users can talk to your agent.** [Talk to your agent from any app](/agents/talk-to-an-agent) shows the sign-in with `ProofableSignIn` and the call as the person: `agentChat` with `agentWallet` runs your public agent as a template in their own profile, on their records and credits, with nothing of yours in the conversation. +- **Your app's users can talk to your agent, and your app pays.** [Talk to your agent from any app](/agents/talk-to-an-agent) shows the sign-in with `ProofableSignIn`, the call as the person (`agentChat` with `agentWallet` runs your public agent as a template in their own profile, with nothing of yours in the conversation), and the billing: with `appId` and `billingWallet` the turn carries your app's sponsor grant and your organization pays within the daily cap you signed. The refusal table gains `SPONSOR_CAP_EXCEEDED`. - **Benchmark.** [Benchmark](/benchmark) replaces the Evidence page. It opens on one table of outcomes, each beside the setup it was measured against: 48 of 48 governance scenarios, every out-of-permission action stopped before it runs, 0 of 6 data-theft attempts out of a hijacked agent, 150 of 150 forged records caught, $0.024 against $0.078 per handed-off result, reuse 750 times faster than redoing the work, 42 times fewer tokens for agent memory, a 3.1 times lower worst-case research spend, and one identity check reused by 100 apps for $1.75 against $150. How each result was measured is in one section for reviewers. `/evidence`, `/benchmarks` and `/use-cases/authority-at-dispatch` open it. diff --git a/agents/talk-to-an-agent.mdx b/agents/talk-to-an-agent.mdx index 8d50672..48133a9 100644 --- a/agents/talk-to-an-agent.mdx +++ b/agents/talk-to-an-agent.mdx @@ -31,29 +31,41 @@ Sign the client in with an [access key](/deployment/access-keys) on a server, or ## Let your app's users talk to your agent -Sign your users in with Proofable, then make the same call as them. Each person talks to your public agent inside their own Proofable profile: their records, their credits, and nothing of yours in the conversation. What the agent knows is its instructions and the records you made public. +Sign your users in with Proofable, then make the same call as them. Each person talks to your public agent inside their own Proofable profile: their records, and nothing of yours in the conversation. What the agent knows is its instructions and the records you made public. Your app pays. ```javascript import { ProofableClient, ProofableSignIn } from '@proofable/sdk'; -// 1. Sign-in. Register your callback once and keep the client id. const signIn = new ProofableSignIn({ redirectUri: 'https://your.app/proofable/callback', clientId }); -const { url, state, verifier } = await signIn.start(); // send the person to url; keep state and verifier -const tokens = await signIn.finish({ code, verifier, state, expectedState }); // when they are back -// 2. Act as the person. -const person = new ProofableClient({ apiKey: tokens.accessToken }); +// 1. Send the person to Proofable. Keep `state` and `verifier` until they are back; a signed cookie works. +const started = await signIn.start(); +redirect(started.url); + +// 2. They are back at your callback with `code` and `state` in the URL. +const tokens = await signIn.finish({ code: query.code, verifier: kept.verifier, state: query.state, expectedState: kept.state }); + +// 3. Act as the person, billed to your app. +const person = new ProofableClient({ + apiKey: tokens.accessToken, + appId: 'your-app', + billingWallet: '0xYourAccount', + appOrigin: 'https://your.app', +}); const who = await person.me(); // { handle, address, ... } const turn = await person.agentChat({ agentId: 'my-agent', - agentWallet: '0xYourAccount', // your account: the agent runs as a template in theirs + agentWallet: '0xYourAccount', // your account: the agent runs as a template in the person's profile + turnId: `u-${yourUserKey}-${Date.now()}`, // your key for the person; every spend record carries it messages: [{ role: 'user', content: 'Where does the case start?' }], }); ``` -What the agent may do for the person is fixed: read and add to the person's own records, and search the web. It cannot spend, grant, change keys or run code, and a high-risk action waits for the person. When it keeps words the person confirms, the record is the person's own, and they can open it at `proofable.me/proof/`. +**Sign-in** is the one every MCP host uses ([MCP OAuth](/mcp/oauth)). `start()` with no `clientId` registers your callback and returns the id; store it with your app's settings and pass it to every later instance. Renew with `signIn.refresh(tokens.refreshToken)` before the token expires. -The sign-in is the one every MCP host uses ([MCP OAuth](/mcp/oauth)). `start()` with no `clientId` registers your callback and returns the id; store it with your app's settings. Renew with `signIn.refresh(tokens.refreshToken)` before the token expires. +**Who pays.** Without `appId` and `billingWallet`, each person spends their own credits. With them, the client sends your app's sponsor grant with each turn and your organization is billed, within the daily cap you signed, with the spend recorded under your app and your `turnId`. The one-time setup is a Sponsor Authorization on your account naming the app, its origin, the scope `agent-chat` and the daily cap; it is the same authorization that sponsors your users' checks, and per-person limits are yours to apply from that record of spend. + +**What the agent may do** for the person is fixed: read and add to the person's own records, and search the web. It cannot spend, grant, change keys or run code, and a high-risk action is refused. The agent must be public or link-only on your account. When it keeps words the person confirms, the record is the person's own, and they can open it at `proofable.me/proof/`. ## What happens on each turn @@ -94,18 +106,21 @@ The stream sends `turn.start`, `answer.delta` as the answer arrives, `tool.start } ``` +For someone else's public agent, `delegationQHash` is `null`: there is no permission between you, the agent ran as a template in your profile. + A request Proofable refuses never opens the stream. It answers JSON with the reason: | Status | Code | Why | |---|---|---| | 401 | `AUTH_REQUIRED` | No session, OAuth token, or access key | -| 402 | `INSUFFICIENT_CREDITS` | Not enough credits for a hosted turn | -| 403 | `AGENT_DELEGATION_REQUIRED` | No active permission from your account to this agent | +| 402 | `INSUFFICIENT_CREDITS` | Not enough credits for a hosted turn, on the account that pays | +| 402 | `SPONSOR_CAP_EXCEEDED` | The sponsoring app has used its daily cap | +| 403 | `AGENT_DELEGATION_REQUIRED` | No active permission from your account to this agent, and no public agent of that id at `agentWallet` | | 403 | `ADVANCED_REQUIRES_PRO` | Advanced runs on paid plans | ## What it costs -A hosted turn spends credits from the account that calls it, by the tokens it used and the model that served them, the same rate as a [job](/jobs) step. A turn on your own model key spends no platform credits. See [pricing](/pricing). +A hosted turn spends credits from the account that calls it, by the tokens it used and the model that served them, the same rate as a [job](/jobs) step. A turn your app sponsors spends the app owner's credits instead, within the app's daily cap. A turn on your own model key spends no platform credits. See [pricing](/pricing). Read your balance and spend at any time, by line and by day: From 5d414937347e3e0ab02ceef9ccabbca62f4aa7b6 Mon Sep 17 00:00:00 2001 From: chrisjleal <184772742+chrisjleal@users.noreply.github.com> Date: Tue, 3 Nov 2026 20:45:48 -0600 Subject: [PATCH 3/3] Talk to your agent from any app: your app pays, turned on with one command Who pays, the proofable sponsor command, the call as the person, what the agent may do for them, what happens on each turn, the refusals an app can meet, and the public API spec. --- CHANGELOG.md | 2 +- agents/talk-to-an-agent.mdx | 26 +++++++++++++++++--------- openapi/public-api.json | 8 ++++---- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5157316..70b9d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Product release notes: [docs.proofable.me/changelog](https://docs.proofable.me/c ### Added -- **Your app's users can talk to your agent, and your app pays.** [Talk to your agent from any app](/agents/talk-to-an-agent) shows the sign-in with `ProofableSignIn`, the call as the person (`agentChat` with `agentWallet` runs your public agent as a template in their own profile, with nothing of yours in the conversation), and the billing: with `appId` and `billingWallet` the turn carries your app's sponsor grant and your organization pays within the daily cap you signed. The refusal table gains `SPONSOR_CAP_EXCEEDED`. +- **Your app's users can talk to your agent, and your app pays.** [Talk to your agent from any app](/agents/talk-to-an-agent) shows the sign-in with `ProofableSignIn`, the call as the person (`agentChat` with `agentWallet` runs your public agent in their own profile, with nothing of yours in the conversation), and the billing: `proofable sponsor` turns sponsorship on once, and a client with `appId` and `billingWallet` makes your organization the payer within the daily cap. The refusal table gains `SPONSOR_CAP_EXCEEDED`. - **Benchmark.** [Benchmark](/benchmark) replaces the Evidence page. It opens on one table of outcomes, each beside the setup it was measured against: 48 of 48 governance scenarios, every out-of-permission action stopped before it runs, 0 of 6 data-theft attempts out of a hijacked agent, 150 of 150 forged records caught, $0.024 against $0.078 per handed-off result, reuse 750 times faster than redoing the work, 42 times fewer tokens for agent memory, a 3.1 times lower worst-case research spend, and one identity check reused by 100 apps for $1.75 against $150. How each result was measured is in one section for reviewers. `/evidence`, `/benchmarks` and `/use-cases/authority-at-dispatch` open it. diff --git a/agents/talk-to-an-agent.mdx b/agents/talk-to-an-agent.mdx index 48133a9..9ad5002 100644 --- a/agents/talk-to-an-agent.mdx +++ b/agents/talk-to-an-agent.mdx @@ -36,7 +36,8 @@ Sign your users in with Proofable, then make the same call as them. Each person ```javascript import { ProofableClient, ProofableSignIn } from '@proofable/sdk'; -const signIn = new ProofableSignIn({ redirectUri: 'https://your.app/proofable/callback', clientId }); +// clientId: what register() returned the first time, kept with your app's settings. +const signIn = new ProofableSignIn({ redirectUri: 'https://your.app/proofable/callback', clientId: settings.proofableClientId }); // 1. Send the person to Proofable. Keep `state` and `verifier` until they are back; a signed cookie works. const started = await signIn.start(); @@ -55,7 +56,7 @@ const person = new ProofableClient({ const who = await person.me(); // { handle, address, ... } const turn = await person.agentChat({ agentId: 'my-agent', - agentWallet: '0xYourAccount', // your account: the agent runs as a template in the person's profile + agentWallet: '0xYourAccount', // your account: the agent runs in the person's own profile turnId: `u-${yourUserKey}-${Date.now()}`, // your key for the person; every spend record carries it messages: [{ role: 'user', content: 'Where does the case start?' }], }); @@ -63,15 +64,21 @@ const turn = await person.agentChat({ **Sign-in** is the one every MCP host uses ([MCP OAuth](/mcp/oauth)). `start()` with no `clientId` registers your callback and returns the id; store it with your app's settings and pass it to every later instance. Renew with `signIn.refresh(tokens.refreshToken)` before the token expires. -**Who pays.** Without `appId` and `billingWallet`, each person spends their own credits. With them, the client sends your app's sponsor grant with each turn and your organization is billed, within the daily cap you signed, with the spend recorded under your app and your `turnId`. The one-time setup is a Sponsor Authorization on your account naming the app, its origin, the scope `agent-chat` and the daily cap; it is the same authorization that sponsors your users' checks, and per-person limits are yours to apply from that record of spend. +**Who pays.** Your app does. Turn it on once, signed in as yourself: -**What the agent may do** for the person is fixed: read and add to the person's own records, and search the web. It cannot spend, grant, change keys or run code, and a high-risk action is refused. The agent must be public or link-only on your account. When it keeps words the person confirms, the record is the person's own, and they can open it at `proofable.me/proof/`. +```bash +npx -y @proofable/sdk sponsor --app your-app --origin https://your.app --credits-per-day 2000 +``` + +From then on a client built with `appId` and `billingWallet` carries your sponsorship: your organization pays for your users' checks and their chats with your agents, within the daily cap you set, and every spend record names your app and your `turnId`, so limits per person are yours to apply. Without `appId` and `billingWallet`, each person spends their own credits. In code, the same step is `client.sponsorApp({ appId, origin, creditsPerDay })`. + +**What the agent may do** for the person is fixed: read and add to the person's own records, and search the web. It cannot spend, grant, change keys or run code, and a high-risk action is refused. The agent must be public or link-only on your account. When it keeps words the person confirms, the record is the person's own, and they can open it at `proofable.me/proof/`. Keeping it is one credit from the person's own allowance, which every account has; the turns are what your app pays. ## What happens on each turn -1. Proofable finds the [permission](/agents/agent-delegation) you signed for the agent. No active permission from your account, no turn. -2. The turn runs as the agent: its own instructions and skills from its [identity](/agents/agent-identity), and the connected apps you bound to it. -3. Every tool call is checked against the permission before it runs, read again for each call. Revoke the permission and the next call stops, in every app that talks to the agent. +1. Proofable finds the [permission](/agents/agent-delegation) you signed for the agent. With none, the turn runs only if `agentWallet` names someone else's public agent, in your own profile, under the fixed rule above. +2. The turn runs as the agent: its own instructions and skills from its [identity](/agents/agent-identity), and, for your own agent, the connected apps you bound to it. +3. Every tool call is checked before it runs, against the permission or the fixed rule, read again for each call. Revoke the permission and the next call stops, in every app that talks to the agent. 4. The answer comes back with the records the agent read. Each one opens at `proofable.me/proof/`, so anyone you share it with can check where the answer came from. ## Over HTTP @@ -88,7 +95,7 @@ curl -N https://api.proofable.me/api/v1/agent/chat/turn \ | Field | What it does | |---|---| | `agentId` | Run the turn as this agent. Omit it to run as yourself. | -| `agentWallet` | The agent's account: when two of your agents share an id, or to talk to someone else's public agent, which then runs as a template in your own profile | +| `agentWallet` | The agent's account: when two of your agents share an id, or to talk to someone else's public agent, which then runs in your own profile | | `messages` | The conversation, `user` and `assistant` turns, ending with the user | | `systemPrompt` | Context from your app. The agent reads it as data, after its own instructions. | | `model` | `default` or `advanced`. With `provider`, that provider's model id. | @@ -106,7 +113,7 @@ The stream sends `turn.start`, `answer.delta` as the answer arrives, `tool.start } ``` -For someone else's public agent, `delegationQHash` is `null`: there is no permission between you, the agent ran as a template in your profile. +For someone else's public agent, `delegationQHash` is `null`: there is no permission between you, the agent ran in your own profile. A request Proofable refuses never opens the stream. It answers JSON with the reason: @@ -116,6 +123,7 @@ A request Proofable refuses never opens the stream. It answers JSON with the rea | 402 | `INSUFFICIENT_CREDITS` | Not enough credits for a hosted turn, on the account that pays | | 402 | `SPONSOR_CAP_EXCEEDED` | The sponsoring app has used its daily cap | | 403 | `AGENT_DELEGATION_REQUIRED` | No active permission from your account to this agent, and no public agent of that id at `agentWallet` | +| 403 | `SPONSOR_GRANT_INVALID` | The app's sponsor grant was sent but not accepted: expired, already used, for another app or origin, or for an agent that is not the sponsoring account's. The person is never charged instead | | 403 | `ADVANCED_REQUIRES_PRO` | Advanced runs on paid plans | ## What it costs diff --git a/openapi/public-api.json b/openapi/public-api.json index 330d2d3..1660b99 100644 --- a/openapi/public-api.json +++ b/openapi/public-api.json @@ -4838,7 +4838,7 @@ "post": { "operationId": "streamAgentChatTurn", "summary": "Talk to an agent: run one chat turn as it, streamed as Server-Sent Events", - "description": "Runs one turn on the hosted agent runtime. With agentId the turn runs as that agent: its instructions and skills, under the delegation you signed, every tool call checked against it. Name agentWallet to talk to someone else's public agent: it then runs as a template in your own profile, on your records and credits, and nothing of its owner's is part of the conversation. The final event carries the answer, the records it read as citations, and the credits it spent. Nothing to deploy: call it from any app or runtime with a session, an OAuth token or a profile access key.", + "description": "Runs one turn on the hosted agent runtime. With agentId the turn runs as that agent: its instructions and skills, under the delegation you signed, every tool call checked against it. Name agentWallet to talk to someone else's public agent: it then runs in your own profile, on your records, and nothing of its owner's is part of the conversation; with your app's sponsor grant, your app pays. The final event carries the answer, the records it read as citations, and the credits it spent. Nothing to deploy: call it from any app or runtime with a session, an OAuth token or a profile access key.", "tags": [ "Agents" ], @@ -4859,7 +4859,7 @@ }, "agentWallet": { "type": "string", - "description": "The agent's account: when two of the caller's agents share an agentId, or to talk to someone else's public agent, which then runs as a template in the caller's own profile." + "description": "The agent's account: when two of the caller's agents share an agentId, or to talk to someone else's public agent, which then runs in the caller's own profile, with nothing of its owner's in the conversation." }, "messages": { "type": "array", @@ -4926,10 +4926,10 @@ "description": "Authentication required" }, "402": { - "description": "Not enough credits for a hosted turn" + "description": "Not enough credits for a hosted turn on the account that pays, or the sponsoring app has used its daily cap (SPONSOR_CAP_EXCEEDED)" }, "403": { - "description": "No active delegation for this agent, or Advanced without a paid plan" + "description": "No active delegation for this agent and no public agent of that id at agentWallet, Advanced without a paid plan, or a sponsor grant that was not accepted (SPONSOR_GRANT_INVALID)" }, "404": { "description": "Not found",