Entity Gateway
One URL for every model call. The agent sends a gateway token or a signed request; the gateway resolves its DID, checks grants and budgets, runs your guard hooks, adds the provider key from your vault, streams the answer back unchanged, and records the call. The agent never holds a provider key. The gateway is part of every Agent Secret Store plan; see pricing.
1. Get a gateway token
The agent needs a verified DID (Agent Identity). Ask for a scoped token with gateway:invoke:*. An agent that signed itself up (Agent Self-Signup) already has one.
curl -s -X POST https://api.agentsecretstore.com/v1/tokens \
-H "Authorization: Bearer $ASS_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"scopes": ["gateway:invoke:*"], "ttl": "1h"}'
# {"token": "eyJ...", "token_id": "...", "expires_at": "...", "requires_approval": false, "approval_id": null}The token goes wherever your client already puts its provider key. Without a token, send a signed request (step 3).
2. Point your client at the gateway
- 1
Claude Code
terminalexport ANTHROPIC_BASE_URL=https://gateway.agentsecretstore.com export ANTHROPIC_API_KEY="eyJ..." # the gateway token, not a provider key claude - 2
Codex and the OpenAI SDKs
terminalexport OPENAI_BASE_URL=https://gateway.agentsecretstore.com/v1 export OPENAI_API_KEY="eyJ..." # the gateway token codexcurlcurl -s https://gateway.agentsecretstore.com/v1/chat/completions \ -H "Authorization: Bearer $GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "hi"}]}'The Responses API works the same way at
/v1/responses. - 3
Anthropic SDKs and curl
curlcurl -s https://gateway.agentsecretstore.com/v1/messages \ -H "x-api-key: $GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"model": "claude-sonnet-4-5", "max_tokens": 256, "messages": [{"role": "user", "content": "hi"}]}' - 4
Gemini
Base path
/v1beta. The token goes in?key=orx-goog-api-key. BothgenerateContentandstreamGenerateContentwork.curlcurl -s "https://gateway.agentsecretstore.com/v1beta/models/gemini-2.5-flash:generateContent?key=$GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"contents": [{"parts": [{"text": "hi"}]}]}' - 5
Amazon Bedrock
Path
/bedrock/model/{model}/invoke, Bearer token. The provider entry in your config needs abase_urlfor the Bedrock runtime region you use.curlcurl -s https://gateway.agentsecretstore.com/bedrock/model/anthropic.claude-sonnet-4-5/invoke \ -H "Authorization: Bearer $GATEWAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"anthropic_version": "bedrock-2023-05-31", "max_tokens": 256, "messages": [{"role": "user", "content": "hi"}]}'
Nothing is translated
A request goes only to providers that speak its wire format: OpenAI formats to OpenAI or any OpenAI-compatible endpoint, Anthropic to Anthropic, Gemini to Gemini, Bedrock to Bedrock. GET /v1/models lists the models your DID may use (explicit names and aliases; wildcard patterns are not listed).
3. Sign requests instead (RFC 9421)
An agent can prove its DID with an HTTP message signature and carry no token at all. The profile is the Web Bot Auth profile of the Open Entity standard (signed requests guide): Ed25519, the keyid is the agent's did:key, covered components are "@method" "@target-uri" "content-digest", and created, expires and nonce are required. The window is at most 300 seconds and a nonce works once. The signed URL must be the gateway's own public URL. If the DID is enrolled in several vaults, add X-Entity-Gateway-Tenant: <tenant uuid>.
import json, httpx
import openentity as oe
kp = oe.KeyPair(seed) # the agent's Ed25519 key (32-byte seed)
url = "https://gateway.agentsecretstore.com/v1/chat/completions"
body = json.dumps({"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "hi"}]}).encode()
headers = oe.httpsig.sign_request("POST", url, body, kp, kp.did) # Signature-Input, Signature, Content-Digest
r = httpx.post(url, content=body, headers={**headers, "Content-Type": "application/json"})import { signRequest } from "@openentity/sdk";
const url = "https://gateway.agentsecretstore.com/v1/chat/completions";
const body = new TextEncoder().encode(
JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hi" }] }),
);
const headers = await signRequest({ method: "POST", url, body, privateKey: seed, did });
const res = await fetch(url, { method: "POST", body, headers: { ...headers, "Content-Type": "application/json" } });A provider with "auth_mode": "passthrough" forwards the client's own provider key instead of injecting one from the vault. That needs a signed request, because the bearer slot otherwise holds the gateway token, which is never sent to a provider.
4. Configure providers, grants, budgets and hooks
The gateway reads one config per vault, written in HuJSON (JSON with comments and trailing commas). Put the provider keys in the vault first, then reference them as namespace/key.
# once, with a token that has secrets:write:gateway/*
ass secrets set gateway/ANTHROPIC_API_KEY # value on stdin
ass secrets set gateway/OPENAI_API_KEY
ass secrets set gateway/CLAWGRC_API_KEY # ClawGRC key for the evidence exporterWriting the config needs the vault owner's sign-in or a token with gateway:admin:*. A token can only name secrets it can read, so add secrets:read for each reference.
curl -s -X POST https://api.agentsecretstore.com/v1/tokens \
-H "Authorization: Bearer $ASS_AGENT_KEY" -H "Content-Type: application/json" \
-d '{"scopes": ["gateway:admin:*", "secrets:read:gateway/*"], "ttl": "1h"}'{
// Providers: keys are vault references, never values.
"providers": [
{ "id": "anthropic", "type": "anthropic", "secret": "gateway/ANTHROPIC_API_KEY", "models": ["claude-*"] },
{ "id": "openai", "type": "openai", "secret": "gateway/OPENAI_API_KEY", "models": ["gpt-*", "o*"] },
{ "id": "gemini", "type": "gemini", "secret": "gateway/GEMINI_API_KEY", "models": ["gemini-*"] },
{ "id": "bedrock", "type": "bedrock", "base_url": "https://bedrock-runtime.us-east-1.amazonaws.com",
"secret": "gateway/BEDROCK_API_KEY", "models": ["anthropic.*"] },
],
// Clients can ask for "default"; the gateway rewrites it.
"model_aliases": { "default": "claude-sonnet-4-5" },
// Deny by default. A request needs one grant that matches its caller and model.
"tags": { "coders": ["did:key:z6MkExampleCoderOne", "did:key:z6MkExampleCoderTwo"] },
"roles": { "admin": ["did:key:z6MkExampleLead"] },
"grants": [
{ "id": "coders", "src": ["tag:coders"], "models": ["claude-*", "gpt-*"] },
{ "id": "verified-agents", "src": ["tier:>=2"], "models": ["claude-*"] },
{ "id": "admins", "src": ["role:admin"] },
],
// Dollar budgets. "<did>" gives every agent its own bucket.
"budgets": [
{ "id": "per-agent", "key": "<did>", "capacity_usd": 5, "refill_usd_per_day": 5,
"overdraft": "team-pool", "on_empty": "ask" },
{ "id": "team-pool", "key": "<tenant>", "capacity_usd": 200, "refill_usd_per_day": 100 },
],
// Secure My Agent Guard runs before every call; free on every plan. If it is down, the call stops.
"hooks": [
{ "id": "guard", "preset": "securemyagent-guard", "on_error": "fail_closed" },
],
// Evidence out: every completed call goes to ClawGRC.
"exporters": [
{ "id": "clawgrc", "type": "clawgrc", "url": "https://api.clawgrc.com/v1/gateway/events",
"secret": "gateway/CLAWGRC_API_KEY" },
],
"retention": { "mode": "metadata", "days": 30 },
}- Grants match a caller by
did:...,agent:<id>,tag:name,role:nameortier:>=N(N from 0 to 4, the agent's trust tier in the Open Entity registry), and a list of model patterns. - Budgets are dollar buckets that refill daily.
keyis a fixed name or<did>,<agent>,<tenant>. Anoverdraftbudget catches what an empty bucket cannot cover.on_emptyisblock(429) orask(409, a human approves). - Retention is
full,metadataorzero. What each plan allows is on the pricing page. - Exporters:
clawgrc,otlp,splunk_hec,gcsands3. Theclawgrcexporter feeds ClawGRC gateway evidence. - Provider, hook and exporter URLs must be HTTPS and resolve to public addresses.
The Secure My Agent guard hook
{"id": "guard", "preset": "securemyagent-guard"} is a managed hook: it takes no URL and no secret, and the gateway calls Guard with its own key, so Guard sees every model call before the provider does. It is free on every plan and does not count against Secure My Agent lookup quotas. See Guard every model call.
Use Agent Secret Store's models
No provider key? An agent with a verified DID can call models right away: Grok through the OpenAI formats and Claude through /v1/messages (so Claude Code works). Calls run on Agent Secret Store's own provider accounts and are paid from prepaid credit at the provider's list price plus 5%. Credit never expires, and every verified DID gets a $5 starter credit, once. A vault with no config and no provider of its own for a model uses this by default; to opt in explicitly next to your own providers, add {"id": "platform", "type": "platform"}.
# no provider key anywhere: the gateway token is the only credential
export ANTHROPIC_BASE_URL=https://gateway.agentsecretstore.com
export ANTHROPIC_API_KEY="eyJ..."
claude --model claude-haiku-4-5
curl -s https://gateway.agentsecretstore.com/gateway/credits -H "Authorization: Bearer $ADMIN_TOKEN" # balance and ledger
curl -s -X POST https://gateway.agentsecretstore.com/gateway/credits/checkout -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{"amount_usd": 20}' # Stripe Checkout link
curl -s -X POST "https://gateway.agentsecretstore.com/gateway/credits/topup?amount_usd=20" -H "Authorization: Bearer $ASS_AGENT_KEY" # x402, 402 then payWhen the credit runs out the gateway answers 402 in your client's own error format with a link to top up: $5, $20 or $50 by card, or in USDC with x402.
Validate before you publish
config:validate parses the HuJSON, checks every field, resolves the URLs, and checks that every referenced secret exists and that your token may read it. PUT /gateway/config runs the same checks and stores the config only if they pass.
curl -s -X POST https://gateway.agentsecretstore.com/gateway/config:validate \
-H "Authorization: Bearer $ADMIN_TOKEN" --data-binary @gateway.hujson
# {"valid": true, "problems": []}
# {"valid": false, "problems": ["secret gateway/OPENAI_API_KEY is not in the vault"]}
curl -s -X PUT https://gateway.agentsecretstore.com/gateway/config \
-H "Authorization: Bearer $ADMIN_TOKEN" --data-binary @gateway.hujson
# {"valid": true, "version": 4} 422 with the same problems list if it does not validate
curl -s "https://gateway.agentsecretstore.com/gateway/requests?limit=20" -H "Authorization: Bearer $ADMIN_TOKEN"Hook protocol
A hook is any HTTPS endpoint you list in the config. The protocol is entity-gateway-hook/1. Three events: pre_request (synchronous, before the provider call), and request_complete and tool_calls (asynchronous, after the answer, delivered at least once; deduplicate on request_id plus event).
POST https://api.securemyagent.ai/v1/guard/hook
Authorization: Bearer <the hook's secret>
X-Entity-Gateway-Event: pre_request
X-Entity-Gateway-Request-Id: 3f9a...
{
"version": "entity-gateway-hook/1",
"event": "pre_request",
"metadata": { "did": "did:key:z6Mk...", "agent_id": "...", "model": "claude-sonnet-4-5",
"format": "anthropic_messages", "request_id": "3f9a...", "session_id": "..." },
"request_body": { },
"user_message": "...",
"tools": [ { "kind": "declared", "name": "bash" } ],
"grants": ["coders"]
}The hook answers 200 with one of four actions. block stops the call with your 4xx status (default 403) and message in the client's native error shape. modify replaces the request body and passes it to the next hook. ask opens a human approval and returns 409. allow continues.
{"action": "block", "status": 403, "message": "prompt injection pattern in tool result"}
{"action": "modify", "request_body": { }}
{"action": "ask", "message": "deploy to production needs a human"}
{"action": "allow"}- Hooks run in descending
preference. Ablockoraskends the chain. timeout_msis 100 to 10000 (default 2000).on_errorisfail_open(skip the hook) orfail_closed(503). A timeout, a non-200 answer or an invalid body is an error.- The gateway never sends a provider key, a gateway token or a vault value to a hook.
- Asynchronous events also carry
response_bodyandcost(token counts and nanodollars). Retentionzeroremoves the bodies from them.
Errors
Errors come back in the client's own shape, so its SDK raises the exception it expects. Every answer carries X-Entity-Gateway-Request-Id; use it to find the call in GET /gateway/requests. Provider errors pass through unchanged.
| Status | Code | Meaning |
|---|---|---|
| 401 | unauthenticated, bad_signature, replay, unknown_did | No token, a bad or replayed signature, or a DID no agent holds. |
| 403 | no_grant | No grant in your config lets this DID use this model. Everything is denied until a grant allows it. |
| 403 | no_did, scope_denied | The agent has no verified DID, or the token lacks gateway:invoke:*. |
| 403 or the hook's 4xx | blocked | A guard hook answered block. The message is the hook's. |
| 409 | approval_pending | A budget with on_empty ask is empty, or a hook answered ask. A human approves in the dashboard; retry the same request within 10 minutes. |
| 429 | budget_exhausted | A budget is empty. Retry-After says when it refills enough to admit a call. |
| 503 | guard_unavailable | A fail_closed hook did not answer in time. |
| 402 | insufficient_credits | You are using Agent Secret Store's models and the credit is empty. Top up by card or with x402. |
| 404 | model_not_found | No provider in your config serves that model in the client's wire format. |
| 502 | upstream_unavailable | Every provider failed or returned 429 or 5xx. |
HTTP/1.1 403
{"error": {"message": "no grant lets did:key:z6Mk... use o3", "type": "no_grant", "code": "no_grant"}}{"type": "error", "error": {"type": "permission_error", "message": "no grant lets did:key:z6Mk... use claude-x"}}HTTP/1.1 429
Retry-After: 3601
{"error": {"message": "budget per-agent is empty", "type": "budget_exhausted", "code": "budget_exhausted"}}HTTP/1.1 409
X-Entity-Gateway-Approval: 6b1f...
{"error": {"message": "budget per-agent is empty; approval 6b1f... requested", "type": "approval_pending", "code": "approval_pending"}}Past your plan's monthly gateway requests, calls answer 429 in the same shape with a link to the pricing page.
Your agents never see a provider key →
What the gateway does and why.
Agent Self-Signup →
A vault and a DID with no human.
Agent Identity →
Bind an Open Entity DID to a vault agent.
SecureMyAgent →
Move plaintext keys into the vault.