Skip to main content
The co-signer holds the key that authorizes every spend. It never runs in the browser, so the key is never exposed. On each spend request it validates the request against the account’s on-chain policy and the poisoning firewall, then returns a signature or a veto. A spend needs only this signature; the funds are hard-locked to the account’s target and cap, so the co-signer can only push the payment the payer set up, never steal or redirect. Fail-closed: if the firewall’s data source is unreachable, it withholds the signature.

Server endpoint

Expose two routes on the same origin as your app:
  • GET /api/cosign returns the co-signer’s public address, which the browser locks into each new account.
  • POST /api/cosign authorizes a spend and returns a signature or a veto.
cosign reads the account’s real policy from chain (never trusting what the browser claims), runs a LocalCoSigner backed by the SDK poisoning check(), and signs the exact EIP-712 Spend struct the contract will verify.

The policy the co-signer enforces

Even though the contract re-checks these, the co-signer refuses to put its name on a request that violates them:
The co-signer reads the account’s real target from chain and signs only for that address; it never trusts a target the browser supplies. The browser hands it only {account, owner, amount, action}. A script that swaps the destination cannot get a signature, and the contract’s target is locked anyway.
The amount must be within the remaining cumulative cap, and the account must not have expired.
Runs check(owner, target). Vetoes on a block verdict (drainer, lookalike, zero-value bait, or unavailable data). Set vetoOn: 'warning' to also stop new/fresh recipients.

In-process co-signer (tests and local)

For tests or a single-process setup, use LocalCoSigner directly with a pluggable risk source:
The co-signer is a trust point. If it is unreachable, funds are never stranded: after expiry, the vault calls sweepExpired(vault) and the account goes home. No co-signer signature is involved. The call requires msg.sender == vault and that the address match the stored vaultHash, so the escape hatch belongs to whoever holds the vault key, and to nobody else who merely cracked the commitment. Design your key management around that: bind a box to an address you can still transact from when the enclave is down.