# cosigner-client — integrating a bot
The signer abstraction for bots that sign Arch Network transactions through
[`arch-cosigner`](../../README.md). One code path covers a local key and the
proxy, and every proxy response is verified locally (BIP322 over the exact
submitted message, against the configured role pubkey) before the signature
is handed back.
```toml
[dependencies]
cosigner-client = "0.2"
```
(Published on crates.io. From a checkout of this workspace, a path dependency
`{ path = "../arch-custody-proxy/crates/cosigner-client" }` works too.)
## Minimal flow
```rust
use cosigner_client::{ArchSigner, ArchSignerT};
// The environment decides local vs remote (see the env contract below);
// network and intent are set in code.
let signer = ArchSigner::from_env()?
.with_network(bitcoin::Network::Bitcoin)
.with_intent("swap");
// startup preflight, exactly like a key-file flow:
assert_eq!(onchain_state.operator, signer.pubkey());
// 1. build the message as usual
let msg = ArchMessage::new(&[ix], Some(signer.pubkey()), recent_blockhash);
// 2. sign (remote: POST /v1/sign + verification; local: arch_sdk-identical bytes)
let tx = signer.sign_transaction(msg).await?;
// 3. broadcast
rpc.send_transaction(tx).await?;
```
`ArchSigner` is an enum (`Local` / `Remote`); the signing methods live on the
`ArchSignerT` trait, so keep the trait imported wherever you sign. Signers
can also be constructed directly — `ArchSigner::local(keypair)`,
`ArchSigner::local_from_key_file(path)?`,
`ArchSigner::remote(url, token, role, pubkey)` — and tuned with the consuming
builders `with_network`, `with_intent`, `with_retries`, `with_timeout`.
Mixed-signer transactions (ephemeral position mints, IDL buffers): the
signer's own slot goes to the backend, each ephemeral keypair signs locally,
and every signature is placed by its pubkey's position in `account_keys`:
```rust
let tx = signer.sign_transaction_mixed(msg, &[ephemeral_keypair]).await?;
```
Intent labels are per-call handles; `ArchSigner` is `Clone` and the builders
consume `self`:
```rust
let sweeps = signer.clone().with_intent("sweep");
let payouts = signer.clone().with_intent("fulfill_withdrawal");
```
When the digest or Turnkey activity id is needed (reconciliation, shadow-mode
verification), call `sign_message` directly:
```rust
let resp = signer.sign_message(&msg).await?;
// resp: SignResponse {
// signature: [u8; 64], // verified before it is returned
// arch_account_pubkey: [u8; 32],
// digest_hex: Option<String>, // Some from the proxy, None for local
// turnkey_activity_id: Option<String>, // Some from the proxy, None for local
// }
```
## Environment contract
Backend selection is environment-driven; the network never is. There is no
`COSIGNER_NETWORK` or `ARCH_NETWORK` — the network defaults to
`bitcoin::Network::Bitcoin` and is set only in code, via `with_network`.
| `COSIGNER_URL` | remote | proxy base URL, e.g. `https://cosigner.internal:9080` |
| `COSIGNER_TOKEN` | remote | the role's bearer token |
| `COSIGNER_ROLE` | remote | role name, e.g. `oracle` |
| `COSIGNER_PUBKEY` | remote | the role's Arch account key, 64 hex chars |
| `ARCH_KEY_PATH` | local | key file in `arch_sdk::with_secret_key_file` format |
Every variable also exists in a prefixed form (`ORACLE_COSIGNER_TOKEN`, …)
read by `ArchSigner::from_prefixed_env("ORACLE")`. Resolution follows three
rules:
1. The backend is decided at the most specific level that expresses one: if
the prefixed level has `<P>_COSIGNER_URL` or `<P>_ARCH_KEY_PATH`, it
decides remote vs local; otherwise the bare level decides. `from_env()`
reads the bare level only, and `from_prefixed_env("")` behaves exactly
like `from_env()`.
2. Both `…COSIGNER_URL` and `…ARCH_KEY_PATH` present at the deciding level
is ambiguous and returns `SignError::Config`.
3. After the backend is chosen, each variable fills per-variable,
prefixed-first (`ORACLE_COSIGNER_TOKEN`, else `COSIGNER_TOKEN`).
Empty-string values count as unset. Once a URL decides remote, token, role,
and pubkey are required; the `Config` error lists every missing variable at
once.
Rule 3 is what supports one shared URL with per-bot tokens on a host running
several bots:
```bash
COSIGNER_URL=https://cosigner.internal:9080 # shared by every bot on the host
ORACLE_COSIGNER_TOKEN=... # per-bot
ORACLE_COSIGNER_ROLE=oracle
ORACLE_COSIGNER_PUBKEY=<64 hex>
```
`ArchSigner::from_prefixed_env("ORACLE")?` yields a remote signer for the
shared URL carrying the oracle's token, role, and pubkey.
## Mocking
`ArchSignerT` has two required items — `pubkey` and `sign_message`. The
transaction methods (`sign_transaction`, `sign_transaction_mixed`) are
provided by the trait, so a test double inherits the real
signature-placement algorithm:
```rust
struct TestSigner(UntweakedKeypair);
#[async_trait]
impl ArchSignerT for TestSigner {
fn pubkey(&self) -> Pubkey {
Pubkey::from_slice(&XOnlyPublicKey::from_keypair(&self.0).0.serialize())
}
async fn sign_message(&self, msg: &ArchMessage) -> Result<SignResponse, SignError> {
let signature = arch_sdk::sign_message_bip322(&self.0, &msg.hash(), Network::Bitcoin)
.map_err(|e| SignError::Signing(e.to_string()))?;
Ok(SignResponse { signature, arch_account_pubkey: self.pubkey().serialize(),
digest_hex: None, turnkey_activity_id: None })
}
}
```
The trait is object-safe: code that takes `&dyn ArchSignerT` accepts
`ArchSigner`, either concrete signer, or a mock alike.
## Operational notes
- Errors are ordinary errors — propagate them with `?`. `SignError` has four
variants: `Config`, `Signing`, `Proxy { status, detail }`, `Verification`.
- Retries and timeouts are built in: the remote signer retries 502 and
transport failures (`Proxy { status: None }`) with exponential backoff
(default 2 retries, 250 ms base) behind a 35 s request timeout. Adjust with
`with_retries` / `with_timeout`.
- A bot that wants halt-awareness can match
`SignError::Proxy { status: Some(503), .. }`: the proxy is halted (or the
operator stopped the service). The client does not retry 503; to poll for
recovery, `GET <base_url>/readyz` (via `signer.as_remote()` →
`base_url()`) returns 200 once the proxy serves again.
- `Verification` means the response failed the local BIP322 check — a wrong
pubkey or a bad signature, not something a retry fixes.
- The network set via `with_network` must equal the proxy's `network` config;
a mismatch invalidates every signature.
- Pacing: the proxy handles concurrent requests per role, but Turnkey
sub-orgs cap around 10 RPS — batch or pace anything hotter.
Migrating from 0.1: the symbol-by-symbol mapping lives in
[CHANGELOG.md](CHANGELOG.md). Non-Rust consumers implement the raw HTTP
contract in the [root README](../../README.md)'s API section; `src/lib.rs`
here and `arch-cosigner sign` are the reference implementations.