Agent API · v1 · Policies
How the pipes behave
The Agent API is meant to be boring: the same request gets the same answer, a failed call costs nothing, and nothing changes without a dated entry on the changelog. This page states the rules the code enforces. Every number on it is rendered from the constant the route reads, so it cannot drift from what you will actually hit.
The machine-readable version is GET /api/v1/status. The contract itself (modes, prices, schemas, error codes) is on the API reference.
Versioning
The contract is a versioned public interface under /api/v1. Both rails, coaches at POST /api/v1/agents/{agent_id}/call and actions at POST /api/v1/actions/{slug}/call, follow one rule:
- Adding an optional field, to a request or a response, is a compatible change and ships in place. Parse what you use and ignore the rest.
- Making a field required, renaming one, or narrowing an enum is a breaking change. It ships as
/api/v2alongside/api/v1, never in place of it. - In a failure body, the
errorcode is part of the contract and safe to branch on. Themessageis prose for a human and may change.
The request and response schemas are the single source: the published JSON Schema, the OpenAPI document and the API reference are generated from the same definitions the routes validate against, so the docs cannot describe a field the route does not check.
Deprecation
Nothing has been deprecated. When something is, this is what happens:
- It is announced on the changelog with kind
deprecated, the date, and what replaces it. The replacement ships before the announcement, so there is always something to move to. - The deprecated path keeps answering for at least 90 days after the announcement. A quarterly release cadence on your side picks it up in the normal course of work.
Prices
Prices can change, and the changelog records each change as kind pricing. What settles is exactly the amount your wallet signed. The exact-payment scheme's facilitator refuses a payment whose signed value differs from the amount required when it verifies, so a price that moves between your 402 and your paid retry produces a fresh 402 carrying the new amount, never a settlement at an amount you did not sign. A price change is never applied to a receipt after the fact.
Rate limits
Payment is the real rate limiter. These exist to bound a client stuck in a loop, not to shape use, and every one of them answers with a 4xx or 5xx before anything settles, so tripping a limit never costs you a payment. Honour Retry-After when it is present.
| Limit | Value | When you exceed it |
|---|---|---|
| Calls per address | 600 per 10 minutes, sliding | 429 rate_limited with Retry-After set to the window (600 seconds). One bucket for coach and action calls. Too many requests from your address. Honour the Retry-After header. |
| Sandbox calls per address | 200 per 10 minutes, sliding | The same 429, from a separate bucket. A request carrying x-gardenpatch-sandbox never counts against the paid allowance, so free traffic cannot starve a paying caller, including two customers behind one address. |
| Characters per call, Triage tier | 3,200 across messages and context | 400 input_too_large before a price is quoted; the body names the cap and the count. Your prompt exceeds the character cap the tier pays for. Shorten it, or call a higher tier. |
| Characters per call, Artifact tier | 6,500 across messages and context | 400 input_too_large before a price is quoted; the body names the cap and the count. Your prompt exceeds the character cap the tier pays for. Shorten it, or call a higher tier. |
| Characters per call, Operating doc tier | 33,000 across messages and context | 400 input_too_large before a price is quoted; the body names the cap and the count. Your prompt exceeds the character cap the tier pays for. Shorten it, or call a higher tier. |
| Idempotency-Key length | 255 characters | 400 invalid_idempotency_key, before a price is quoted. |
| Identical request already running | One at a time per key | 409 request_in_flight with a short Retry-After; come back and collect the deliverable. If the first call dies mid-flight its claim frees itself within 6 minutes, so nothing can lock you out of your own key for long. |
| Daily inference ceiling | Set per deployment, not published | 503 capacity. We are shedding load. Honour Retry-After — it is set to an hour, not seconds. |
The per-address numbers are environment-tunable and GET /api/v1/status reports the live ones. When the limit store is unreachable the coach rail fails open (a paying caller is served without the limit) and the action rail fails closed (503 idempotency_unavailable, nothing sent, nothing charged), because a duplicate text to a real customer cannot be recalled and a lost sale can.
What we store, and for how long
Two things are stored about a paid call, both in a key-value store with an expiry set at write time. There is no database of calls, and there is no copy of what you sent.
| What | How long | Why |
|---|---|---|
| Your request body | Not stored | It is reduced to a SHA-256 fingerprint of the canonical JSON, which is one input to the idempotency key and is kept only as part of the replay record below. Your messages and context are sent to the model provider to produce the answer and are not written anywhere by us. |
| Replay record, under an Idempotency-Key you sent | 24 hours | The response we returned, its status, your payer address, the scheme and network, the settlement header, the price, the request fingerprint and timestamps. This is what an identical retry is served from instead of a second run and a second charge. |
| Replay record, under a derived key | 10 minutes | Same record, when you sent no key and we derived one from your wallet and body. Shorter because a derived key is a guess at intent: it exists to catch a transport retry, which happens within seconds, not to decide that an identical call hours later was a mistake. |
| Receipt | 7 days | The same record, keyed by the rcpt_ id in your paid response. Free to read at GET /api/v1/receipts/{id}, never cached, 404 after expiry. Longer than the replay window because it serves a ledger, not a retry. Read receipt.expires_at first: when it is null, nothing was stored. |
| In-flight marker | at most 6 minutes | The claim on your key while a call runs, replaced by the replay record on success and deleted on any refusal. |
| Your address | 10 minutes | The rate-limit window is a set of request timestamps keyed by your network address. It expires with the window and holds nothing else. |
| Demand counters | 35 days | One counter per mode and outcome per UTC day (sandbox, quoted, paid, rejected, failed), so we can see what people want. No caller identity, no bodies, no prompts. A daily aggregate of our own inference spend is kept the same way, with no caller identity. |
Server logs carry warnings and errors: the mode id, the attempt number, and the failure reason (an output that was truncated, a schema miss, a store write that failed, an upstream error). They never carry your messages, your context or the deliverable.
Actions
For an action the replay record holds the response (the action id, the send service's request_id, the channel, queued, the business name) and the fingerprint of the request. The customer's contact details and the consent block are not stored on our side in clear. They travel with the request to the send service, which keeps them with the send record: the consent block (basis, obtained_at, source) is required precisely so that a record exists of who agreed to what and when, if a carrier ever asks. Retention there is that service's; the request_id is how either side references the send.
One deliberate exception: a send whose outcome we could not confirm (send_state: "unknown") is stored under your key for the replay window so that a retry replays that answer instead of texting the customer a second time. A different Idempotency-Key is how you say the second send is intended.
Charges and refunds
- You are charged only on a 2xx. Settlement runs after the handler returns, and only when it returns success. Every 4xx and 5xx is returned before settlement and cancels it: a malformed body, a mode the coach does not sell, an oversized prompt, a rate limit, a capacity refusal, a model failure and our own bugs all cost nothing. The full table is on the API reference.
- A replay is free. Retrying with the same wallet, body and
Idempotency-Keyreturns the stored deliverable withIdempotent-Replay: true. The authorization you signed for the retry is never submitted; it expires unused. - A sandbox call is free and never reaches the payment layer at all.
- An action settles only after the send service has confirmed it queued the message. If settlement then fails, we sent for free. The reverse, charged but never sent, cannot happen: the send completes before the route returns success.
- The amount is the one you signed. See Prices for what happens when a price moves between your quote and your retry.
Refunds
Settlement is an on-chain USDC transfer and there is no automated reversal. If a settled call returned something defective (a body that fails its own published schema, or an empty deliverable), send the receipt id to hello@gardenpatch.xyz. A refund is a manual USDC transfer to the payer address on the receipt, and the receipt is what we check, which is why it is worth keeping.
Support and disclosure
Write to hello@gardenpatch.xyz. A person reads it. Include what lets us find the call without your request body:
- The receipt id (
rcpt_…), fromreceipt.idin the paid response or the tail ofreceipt.url. - The agent or action, the mode, and the UTC time of the call.
- The
Idempotency-Keyyou sent, or the derived one echoed back in theIdempotency-Keyresponse header. - The
errorcode and HTTP status if the call failed.
Do not send your request body unless we ask for it; the receipt id is enough to trace a call.
Security issues go to the same address with security in the subject; we would rather hear about one than read about it. Requests about personal data you or your customers sent go to privacy@gardenpatch.xyz, as described in the privacy policy.
Status
GET /api/v1/status is free, cacheable for a minute, and open to any origin. It is computed from the same functions the paid routes gate on, not from a separate opinion:
okis true only when payments can settle and coach calls can be fulfilled on this deployment. A falseokis accompanied by the flag that explains it.payments.networkandpayments.testnettell you which chain a 402 will name before you fund a wallet.fulfillmentreports each rail separately: coaches, actions, and whether the replay store (receipts and idempotent retries) is on.limitsandretentioncarry the live values of the numbers on this page;contractrepeats the versioning and deprecation rules in one sentence each.
curl -sS https://gardenpatch.xyz/api/v1/statusFound a place where the code and this page disagree? The code is the contract and the page is the bug. Tell us.