Non-custodial signing
The platform holds no user funds and no user keys. A bot trades because the user granted a narrowly scoped, revocable session key — not because we hold their wallet. This page specifies what that means concretely, because non-custodial is easy to claim and easy to quietly break.
The test. If the platform's entire infrastructure were compromised tomorrow, an attacker must not be able to move user funds anywhere the user had not already authorised. If any design fails that test, it is not non-custodial regardless of what the marketing page says.
Where keys live
| Material | Lives | Platform can read? | Notes |
|---|---|---|---|
| Wallet private key | The user's wallet | Never | We never see, request, transmit, or store it. |
| Session key (private half) | Platform HSM / KMS, per user | Sign only | Non-exportable. Signs only whitelisted methods within granted scope. |
| Session key grant | On chain | Read | The authority is the on-chain grant, not a database row. |
| Venue API credentials | Platform secret store | Yes | Read-only market data. Cannot move funds. |
| Anything held by a user bot | Nothing. A user bot receives no key material of any kind. | ||
The session key is the only thing that can sign, it can only sign what the grant allows, and the grant is on chain where the user can inspect and revoke it without our cooperation. That last clause is what makes it real.
Scope of a grant
A grant is not "this bot may trade". It is a specific, bounded authority, and every bound is independently enforced at signing time even though the chain already checked it.
SessionKeyGrant {
user: 0xUSER…
sessionKey: 0xSESS…
venue: CTFExchangeV2 @ 0xEXCH… // exact address, no wildcards
methods: [ postOrder, cancelOrder ] // whitelist, see below
maxNotionalUsd: 5000 // aggregate over grant lifetime
maxPerOrderUsd: 500
collateral: pUSD @ 0xPUSD… // exact token, no wildcards
marketScope: [ … ] // cluster or ≤50 explicit ids
expiresAt: now + 30d // grants always expire
revocable: true // always true; never offer otherwise
}
Method whitelist
| Method | Allowed | Why |
|---|---|---|
postOrder | Yes | The point of the grant. |
cancelOrder | Yes | Required for the kill switch to mean anything. |
transfer / transferFrom | Never | This is custody. There is no legitimate reason a trading bot needs it. |
approve (unbounded) | Never | An unbounded approval is custody with extra steps. |
approve (exact, per grant) | Only at grant time, by the user | Bounded to maxNotionalUsd, never widened by us. |
| Any upgrade, admin, or delegate call | Never | Not in scope for trading; a bypass vector. |
| Anything not listed | Never | Default deny. New methods require an explicit review. |
The failure mode that matters. If the whitelist is not enforced at the signer — if it is only checked in the caller, or only validated in the UI — a compromised strategy or a bug in the order path can sign a transfer. Enforce it at the signer, where it is the last thing that happens before a signature exists.
Grant and revoke
- Preview. The user is shown the grant in plain English before signing: which venue, which markets, which methods, what ceilings, when it expires. Generated from the grant object so it cannot drift from what is actually requested.
- Grant. The user signs from their own wallet. This is the only moment their wallet key is used. We never proxy it.
- Use. The session key signs whitelisted calls within scope. Every signature is logged with the correlation id of the decision that caused it, so any order can be traced back to the rule that proposed it.
- Revoke. One action, effective immediately, available in the product and independently on chain. Revocation must not require our servers to be up.
- Expire. Grants expire on their own. A user who walks away is not left with an open-ended authority.
Behaviour on revoke
- In-flight signing attempts fail with
SEC_GRANT_REVOKED. They are not retried. - Open orders are cancelled if
cancelOrderis still permitted; otherwise the user is told explicitly which orders remain and how to cancel them directly at the venue. - Open positions are untouched. We cannot and must not flatten a position without authority — that would itself be custody.
- Every affected user bot moves to paused, with the reason shown.
- The user sees one clear summary of what happened, not a stream of individual failures.
This is the sequence most likely to be got wrong, because the tempting behaviour — "flatten everything to be safe" — is the one that breaks the custody guarantee.
Signature refused mid-run
A signature can be refused for reasons that are not errors: the grant expired, the notional ceiling is reached, the market fell outside scope, the user revoked. A strategy must treat refusal as a normal outcome.
| Cause | Code | Correct handling |
|---|---|---|
| Grant expired | SEC_GRANT_EXPIRED | Pause the bot. Prompt the user to re-grant. Do not retry. |
| Notional ceiling reached | SEC_NOTIONAL_EXHAUSTED | Stop opening. Allow cancels. Tell the user the ceiling was hit. |
| Market outside scope | SEC_SCOPE_VIOLATION | Reject the proposal and count a strike — the bot should not have proposed it. |
| User revoked | SEC_GRANT_REVOKED | Follow the revoke sequence above. |
| Signer unavailable | SEC_SIGNER_UNAVAILABLE | Fail closed. No order. Alert operations, not the user. |
Never retry a refused signature in a loop, and never fall back to a different key. Both patterns turn a bounded authority into an unbounded one.
What we log, and what we must not
- Log: grant creation and revocation, every signature with its correlation id, every refusal with its cause, notional consumed against ceiling.
- Never log: key material, partial key material, signable payloads in a form that could be replayed, or anything that would let a log reader reconstruct authority.
the four invariants · these are not negotiable
- 1
The platform never holds user funds.
Keys stay in the user's wallet. There is no platform-controlled omnibus account, no internal ledger of custodied balances, and no code path that can move a user's assets without a signature the user authorised. If a feature needs custody to work, it does not ship.
- 2
A user bot never reaches the venue directly.
User-authored logic produces a proposal. It never holds a venue client, an API key, or a signer. Exactly one component signs and submits, and it is ours.
- 3
Guardrails cannot be configured away.
The user tunes thresholds inside bounds we set. The user cannot remove a guardrail from the chain, reorder the chain, or lower a hard cap below the platform floor.
- 4
AI never holds Trade authority.
Model output is advisory. An AI suggestion enters the same RiskGate as any bot intent and is subject to the same rejects. There is no path from a model response to a signed order that skips the chain.
If a design requires breaking one of these, the design is wrong. Escalate before writing code — do not work around it. See RiskGate and the signing model.