Documentation
Guides and reference for OutLayer verifiable compute and agent custody.
Docs navigation
Connectors
What a connector is#
A connector is a curated project that gives an agent named operations on an account that belongs to somebody else — a bank account, a mailbox, a GitHub login, a trading venue — without ever handing the agent the login. It runs in the same TEE as everything else here; what makes it a connector is that its prices live on chain and that we vouch for it. Three parties take part:
- The owner connects the account once, on its page under Integrations: saves the login, writes the rules — which operations, what limits, which payees or repositories, what must wait for them — and names the agents that may use it, for as long as they choose. The login is encrypted to the enclave and locked to the owner's account: the agent never sees it, and neither do we. Taking it back is one step.
- The agent makes one HTTPS call naming the operation, paid with its own payment key. It learns no venue API, signs no orders and holds no key; for the agent a payment, an email or a pull request is one request and one answer.
- The enclave runs the connector's published build: it opens the owner's login inside, checks every rule of the policy — outside the AI, so no prompt argues past it — reaches only the hosts the connector declared, and answers with the proof of what ran.
Asking first. A rule can say that a write waits for the owner: a payment above a line, every email, a merge. The connector then prepares the action whole, leaves it as a task and answers the agent awaiting_owner. The owner reads exactly what will happen in their inbox and approves with one signature of their wallet; the platform runs the connector's confirm as the agent, and that exact action runs, nothing else. The same inbox carries what the agent asks of its owner — a question, a file, a missing value — and the notices it leaves. The owner is told on the site, or at a URL of their own: see Being told.
Nothing on trust. Every call answers with its attestation: the hash of the code that ran, of what went in and of what came out, signed by the Intel chip it ran on. A task in the inbox carries the same proof for the run that prepared it. See Trust & Verification.
Teaching an agent. One URL is all it needs: skills.outlayer.ai/outlayer/SKILL.md creates the wallet and makes the first call; outlayer-connectors/SKILL.md is the model above as the agent reads it — the call, the payment key, the policy row, tasks and their outcomes. Each connector has a skill of its own with its operations, fields and refusals: mercury, gmail, github, hyperliquid, polymarket.
Writing one? See Building one below — the manifest, the network allowlist, the answer format and what can refuse a call to you.
A request to one names the operation at the top of input:
POST /call/connectors.outlayer.near/<connector>
{ "input": { "operation": "send_email", ... } }Each operation has its own price. An operation that is not on the list has no price at all and the call is refused — which is not the same as being free: a price of 0 is a real, published price and means the operation costs nothing beyond compute.
How a call works#
The owner connects once; from then on every call is the agent's. The login is opened only inside the enclave, the rules are checked there, and the agent gets the answer with the proof. When a rule says ask first, the action waits for the owner and runs on their signature. The connector can also ask the owner something — a question, a file, a missing value — or just tell them what happened; the owner hears of it on the site or at a webhook of their own.
Building one#
The list above is the set we built and checked ourselves, not a gate. A connector is an ordinary project you write, publish and price; the sections below are what makes it one, and what can refuse a call to it. The agent-facing half — the operations, their fields and refusals — is the skill you publish beside it.
What makes a project a connector#
A connector is an ordinary project that was curated and priced. There is no separate runtime, no special deployment and no second API. You write a WASI module and publish it like any project; two structural facts make it a connector:
- it is published under the curated namespace —
connectors.outlayer.nearon mainnet,connectors.outlayer.testneton testnet; and - its wasm carries a manifest declaring a
connector_id.
Membership is a comparison against the owner account of the project id, so “is this a connector” is a fact about where it lives rather than something a project claims about itself.
Why the category exists
An ordinary project runs your code for you. A connector runs your code for somebody else's agent, inside a TEE that holds custody keys, and lets it reach the internet. Three constraints follow, and they are the whole difference:
| ordinary project | connector | |
|---|---|---|
| outbound network | open | only the hosts your manifest declares |
| operation naming | your business | a required top-level operation |
| pricing | per call | per operation, on chain |
It is not a plugin or anything running inside another program: your module is a normal WASI guest with a normal entry point, and it cannot see other calls, other agents' secrets, or the wallet's keys.
The shape of a call#
Every connector call names its operation in one place:
{ "operation": "send", "to": "[email protected]", "subject": "hello" }Over HTTPS that object is the input of POST /call/{owner}/{project}; on chain it is input_data in request_execution. The same bytes either way, and four readers take the operation out of them: the contract prices the call, the coordinator bills it and picks which limit applies, the worker refuses a connector call that names none, and your guest dispatches on it.
A connector that acts on the caller's wallet — signs, trades, moves funds — gets that wallet only when the call sends X-Wallet-Id (the wallet's id from GET /wallet/v1/address). Without it the run has no wallet at all; another wallet's id is refused 403 wallet_not_yours.
Fail-closed, before your code runs. Absent, blank, not a string, nested, or spelled op — all refused. None of them defaults, because a defaulted operation is a defaulted price and the cheapest one is what an attacker would pick. An operation with no on-chain price is refused too: unpriced is not free.
Two format constraints: a priced project's request must be a JSON object, and on chain it must be at most 10 KB — the contract parses it and the caller's gas pays for that. A connector that moves more than that takes a reference, not the bytes.
Answering
Return JSON on stdout:
{ "success": true, "output": { }, "logs": [], "error": null }The field is error, not error_message.
Network: you declare it, the worker enforces it#
A connector reaches only the hosts listed in capabilities.network in its manifest. Exact hostnames, case-insensitive, no implicit subdomain wildcard: example.com does not permit evil.example.com.
The manifest lives in a wasm custom section, so it is covered by the SHA256 the contract records for that version. Nobody — not you after publishing, not the operator — can widen it without publishing a new version that users have to move to.
A connector with no manifest section reaches nothing. That is the fail-closed direction, and build.sh in connector-probe fails the build when the section is missing, so you find out at your desk rather than in production.
Every outbound attempt is reported to the coordinator with whether the allowlist permitted it. The coordinator stores that trail and decides nothing — enforcement happens inside the worker, where the keys are.
Three secrets, three owners#
A connector reads secrets exactly as any other project does. What trips connectors up is that three credentials belonging to different people meet in one run.
Yours (the author's). Your SMTP password or upstream API key — the same for every caller. Store it under your own account with store_secrets for the project you publish, and name the profile in the manifest embedded in the wasm; the worker decrypts it into every run, and the call carries nothing:
{ "connector_id": "<id>", "author_secrets": { "profile": "prod" } }Its access condition is judged against the real caller, so it is also who may run your connector: AllowAll for everyone, a whitelist or DAO role for a circle. Not a connector feature: any project declares one the same way — see The author's secret.
The caller's. A row the call names with secrets_ref, gated by the condition its owner stored. An owner hands a credential to their agents by storing it once under their own account and whitelisting the agents' wallet accounts (optionally until a date); each agent names it:
{
"input": { "operation": "send" },
"secrets_ref": { "account_id": "owner.near", "profile": "gmail" }
}The owner's policy, on the trading connectors (hyperliquid, polymarket). For a wallet with an owner the platform attaches the owner's row itself — the call names nothing — keeps a row of the owner's the call does name, and refuses any other account's with 403 policy_row_not_owner; doing that again blocks the wallet on these connectors for a while (403 calls_suspended). With no policy the connector trades on its built-in default: no caps, withdrawals only back to the wallet. A profile per wallet caps that wallet only while the owner's {owner, <connector>} row exists too: a call that names no row runs that one.
The agent's. A credential stored FOR a custody wallet under the wallet's account and fetched only when the call asks for it with x-use-owner-secret: true and names nothing else. You never hold it — see Secrets left FOR an agent.
What can refuse a call to you#
Four independent mechanisms, ANDed — every applicable one must pass, and none of them can raise another.
- Price (the contract). Per operation, with the author's share and the account it pays to.
request_executionrefuses a call that does not attach the operation's exact price. You do not set this in your manifest — a manifest may state a recommended price; the on-chain one is what is charged. - Operation limits (the coordinator).
(operation, window, max, who it applies to). Windows areday/week/month, rolling from first use and not calendar-aligned — a calendar month resets for everybody at midnight on the 1st, which turns a monthly cap into a stampede. - The trial's fifty calls, for a caller on a trial key. A caller who pays has no such count — only the operation limits above.
- The caller's own money — balance, allowance, or per-call cap.
Operation names in the coordinator's own rules carry the connector id (gmail:send, or a whole-segment wildcard gmail:* — no general globbing). In your manifest you write them without it.
Before you ship#
- The manifest section is present in the built wasm, and declares every host you reach.
- Every operation you dispatch on has a price on chain — an unpriced one can never run.
- Your answer uses
error, noterror_message. - On-chain input stays under 10 KB.
- Nothing in your output echoes a secret — yours or the caller's.
wasi-examples/connector-probe in the near-offshore repo is a working connector kept deliberately boring: it does nothing useful so that everything around a connector can be tested against it.
Subscriptions#
A subscription turns a key's per-call charges into a flat allowance for the period the plan runs. Without one, each call takes the compute it used plus the operation's price out of the key's balance. With one, the same calls come out of the allowance instead.
- The allowance belongs to a key, not to an account. One account can hold several keys and subscribe only the one that needs it.
- A key can hold money and an allowance. The allowance is spent first; the balance is what keeps working when it runs out.
- Buying more never shortens what is already paid for — validity extends from whichever is later, today or the current expiry — and paying above a plan's price leaves the difference on the key as spendable balance rather than absorbing it.
- New calls stop being admitted slightly before the expiry, so a call already running is never cut off by the deadline arriving mid-flight.
Where the key comes from. A wallet creates it with POST /wallet/v1/create-payment-key. The answer carries the key string, owner:nonce:secret, shown once — it is what the agent sends as X-Payment-Key on every call, and what GET /subscription/status reads to report what the key has left.
A subscription is bought for a key, by naming it. The purchase is an ft_transfer_call carrying {"action":"buy_subscription","nonce":N,"owner":"<key owner>","plan":0}. The sender pays and the key carries the allowance, so one transaction from your wallet subscribes an agent that owns nothing — and nothing secret is needed to pay.
The wallet's wk_ buys nothing and pays for nothing: it runs the wallet. On /call it is refused as 401 wk_is_not_a_payer. Buying, and choosing where warnings are sent, stay with the owner — a compromised agent should not be able to do them on your behalf.
One subscribed key per agent. A subscription is an attribute any payment key can carry, and an account can hold many keys. Nothing merges them and nothing warns, so two subscribed keys means paying twice for one agent's worth of work.
An ordinary payment key with no subscription is not second-class: it pays per call out of its balance and reaches exactly the same connectors. The subscription is a wholesale rate, not a different class of access.
The Subscription page does it in one step: pick an agent whose key this browser knows, and pay in one transaction.
The trial: fifty calls, in the wallet's first week#
A trial is fifty connector calls, within seven days of the wallet's creation. That is the whole rule. The wallet claims a key with POST /trial-key — under its Bearer wk_… or a Bearer near:… signature — sends it as X-Payment-Key, and the answer says how many calls it makes and when it stops working.
- The week is counted from the wallet's creation, not from the claim: a trial claimed on day six works for one day. Claim it when you register.
- A call counts once it is accepted — any operation, a free one included, and whether or not the run then succeeds. A refused attempt costs nothing.
- The fifty-first call answers
402 trial_exhausted, and any call after the week402 trial_expired. Both are final; the next step is a payment key with money on it. - It reaches the curated connectors and nothing else, cannot pay a developer through
X-Attached-Deposit, and cannot be withdrawn. - There is no balance to read — a trial is not measured in money.
GET /subscription/statusreportstrial.calls_left. - The key is derived, not random:
GET /wallet/v1/payment-keyanswers it again, under the credential that claimed it — nothing needs storing. It is bound to that credential: anotherwk_of the wallet cannot read it (403 payment_key_other_credential), and once thatwk_is revoked the key stops working for good.
One per wallet.
Sponsor codes: premium somebody else pays for#
A sponsor code (spn_…) is a secret a person or a service gives you — a one-time link from a friend, a voucher, or the backend that runs your agent. Redeeming it puts a subscription, paid by the sponsor, on the wallet's nonce-0 key: the trial's key if it was claimed (the trial is converted — no call count), a new key if not.
POST /wallet/v1/sponsorship Authorization: Bearer wk_… (or near:…)
{"code": "spn_…"}
→ {"payment_key": "<owner>:0:<key>", "owner": "…", "nonce": 0,
"allowance_usd": "10000000", "expires_at": "…", "sponsor": "…", "project_ids": […]}- No claim window; it works with or without a trial.
- A key carries one sponsor while its grant is live: the same code again, or another code before the grant ends, changes nothing and answers what the key holds. After it ends, another code is taken — never one this wallet redeemed before.
- Only the credential that reads the wallet's nonce-0 key redeems onto it (another live one is
403 payment_key_other_credential), with one exception, in which the redeem first binds the key to the caller: an older random key goes to the caller that sends it asX-Payment-Key(without it,409 payment_key_not_recoverable). A refused redeem takes no use of the code; a redeem that succeeds always returns the key that now works. - The subscription pays for connector calls. It lifts the free tier's custody limits only when the sponsor set the code up to.
- Refusals:
404 sponsor_code_invalid(one answer for every reason),409 sponsor_cannot_top_up(the key already holds more),409 payment_key_deleted/payment_key_revoked. - A sponsored key may be allowed several calls in flight at once; the code sets how many. Like every nonce-0 key it cannot be bought on: when the grant ends, buy on a payment key the wallet creates.
In the dashboard, New agent takes a code when it creates the agent; a sponsor's link /redeem?code=… opens it with the code filled in. From the CLI: outlayer redeem spn_….
A paying caller has no call quota#
A caller who pays is not limited by any count of connector calls. A funded key is bounded by the money on it; a subscription by its allowance. If a task needs more than the trial, fund a key — there is no quota to wait out.
What remains is technical: a per-key rate limit per minute, one call in flight at a time for a key living on an allowance (a sponsored key: as many as its code allows), and each connector's own ceiling on a single operation, there against a runaway loop and far above ordinary use — Gmail, for one, stops at 500 sends a day per wallet. An attempt refused by that cap still counts toward it, so wait for the window to end rather than retrying into it.