diff --git a/CHANGELOG.md b/CHANGELOG.md index c121533..70b9d28 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, 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. - **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..9ad5002 100644 --- a/agents/talk-to-an-agent.mdx +++ b/agents/talk-to-an-agent.mdx @@ -29,11 +29,56 @@ 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, 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'; + +// 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(); +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 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?' }], +}); +``` + +**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.** Your app does. Turn it on once, signed in as yourself: + +```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 @@ -50,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 | +| `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. | @@ -68,18 +113,22 @@ 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 in your own 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 | `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 -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: diff --git a/openapi/public-api.json b/openapi/public-api.json index f3aaf46..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. 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." + "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",