Rust SDK

Single-file Rust client — ureq + serde. Cargo-ready.

Download

zeq_client.rs online · v1.1.3ureq + serde

Single-file Rust client using ureq (tiny blocking HTTP) and serde. Cargo-ready; three short dependencies.

Contract-parity note: Same v1.1.3 wire format as the Python and JavaScript clients (both smoke-tested live, NM21 → 1.98049e20 N). The Rust client has not been compiled inside our build sandbox because no Rust toolchain is available there. File issues for any cargo/rustc problems — fixes ship same day.

Cargo.toml

[package]
name = "zeq_client_demo"
version = "0.1.0"
edition = "2021"

[dependencies]
ureq       = { version = "2", features = ["json"] }
serde      = { version = "1", features = ["derive"] }
serde_json = "1"

Quickstart

use serde_json::json;
mod zeq_client;
use zeq_client::Client;

fn main() {
    let c = Client::new("");  // reads ZEQ_TOKEN env
    let r = c.compute("NM21", json!({
        "m1": 5.972e24_f64, "m2": 7.342e22_f64, "r": 3.844e8_f64
    })).unwrap();
    println!("{:?} {:?}", r.value, r.unit);  // → Some(1.98049e20) Some("N")
}

Full source — zeq_client.rs

Why inline? The full SDK source is pasted below so the mathematics is auditable. Copy-paste it, read it, self-host it — you never have to trust the Zeq API as a black box. Use our key for convenience, or wire it into your own backend. The v1.1.3 binding contract and ZeqProof HMAC are identical either way.
zeq_client.rs 5730 bytes 156 lines Rust 2021
//! zeq_client.rs — single-file Rust SDK for the Zeq HTTP API (v1.1.3 contract).
//!
//! Zero third-party dependencies beyond the Rust standard library and `ureq`
//! (a tiny blocking HTTP client, ~500 KB with rustls). Runs out of the box:
//!
//! ```toml
//! # Cargo.toml
//! [dependencies]
//! ureq         = { version = "2", features = ["json"] }
//! serde        = { version = "1", features = ["derive"] }
//! serde_json   = "1"
//! ```
//!
//! ```bash
//! ZEQ_TOKEN=zeq_ak_... cargo run --example zeq_client
//! ```
//!
//! Mirrors `zeq_client.py`, `zeq_client.js`, and `zeq_client.go`:
//!
//! - `POST /api/zeq/compute  { operator_id, inputs }` → numeric result
//! - `POST /api/zeq/prove    { operator_id, inputs }` → ZeqProof HMAC
//!
//! [Zeq Daemon] HulyaPulse 1.287 Hz — τ_zeqond = 0.777 s — α_K = 0.00129

use serde::Deserialize;
use serde_json::{json, Value};
use std::{env, fmt};

pub const ALPHA_SPEC: f64 = 0.00129;
pub const F_HULYA: f64    = 1.287;
pub const TAU_ZEQOND: f64 = 0.777;

#[derive(Debug)]
pub enum ZeqError {
    MissingToken,
    Http { status: u16, code: String, message: String, body: String },
    Transport(String),
    Decode(String),
}

impl fmt::Display for ZeqError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ZeqError::MissingToken        => write!(f, "zeq: missing token (set ZEQ_TOKEN or pass to Client::new)"),
            ZeqError::Http { status, code, message, .. } => write!(f, "zeq: {message} (status={status} code={code})"),
            ZeqError::Transport(e)        => write!(f, "zeq: transport error: {e}"),
            ZeqError::Decode(e)           => write!(f, "zeq: decode error: {e}"),
        }
    }
}
impl std::error::Error for ZeqError {}

#[derive(Debug, Deserialize)]
pub struct ComputeResult {
    pub value: Option<f64>,
    pub unit: Option<String>,
    pub uncertainty: Option<f64>,
    pub operator_id: Option<String>,
    pub solver: Option<String>,
    pub binding_overlay: Option<String>,
    #[serde(rename = "zeqProof")]
    pub zeq_proof: Option<String>,
    #[serde(skip)]
    pub raw: Value,
}

pub struct Client {
    pub token: String,
    pub base_url: String,
}

impl Client {
    pub fn new(token: impl Into<String>) -> Self {
        let mut t = token.into();
        if t.is_empty() { t = env::var("ZEQ_TOKEN").unwrap_or_default(); }
        Self { token: t, base_url: "https://www.zeq.dev".into() }
    }

    fn post(&self, path: &str, body: Value) -> Result<Value, ZeqError> {
        if self.token.is_empty() { return Err(ZeqError::MissingToken); }
        let url = format!("{}{}", self.base_url.trim_end_matches('/'), path);
        let resp = ureq::post(&url)
            .set("Content-Type", "application/json")
            .set("Authorization", &format!("Bearer {}", self.token))
            .send_json(body);

        match resp {
            Ok(r) => r.into_json::<Value>().map_err(|e| ZeqError::Decode(e.to_string())),
            Err(ureq::Error::Status(code, r)) => {
                let body = r.into_string().unwrap_or_default();
                let parsed: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
                let msg = parsed.get("error").and_then(|e| e.get("message"))
                    .and_then(|s| s.as_str()).unwrap_or("HTTP error").to_string();
                let errcode = parsed.get("error").and_then(|e| e.get("code"))
                    .and_then(|s| s.as_str()).unwrap_or("HTTP_ERROR").to_string();
                Err(ZeqError::Http { status: code, code: errcode, message: msg, body })
            }
            Err(e) => Err(ZeqError::Transport(e.to_string())),
        }
    }

    pub fn compute(&self, operator_id: &str, inputs: Value) -> Result<ComputeResult, ZeqError> {
        let raw = self.post("/api/zeq/compute", json!({
            "operator_id": operator_id,
            "inputs":      inputs,
        }))?;
        Ok(unpack(raw))
    }

    pub fn prove(&self, operator_id: &str, inputs: Value) -> Result<ComputeResult, ZeqError> {
        let raw = self.post("/api/zeq/prove", json!({
            "operator_id": operator_id,
            "inputs":      inputs,
        }))?;
        Ok(unpack(raw))
    }
}

fn unpack(raw: Value) -> ComputeResult {
    let mut r: ComputeResult = serde_json::from_value(raw.clone()).unwrap_or(ComputeResult {
        value: None, unit: None, uncertainty: None, operator_id: None,
        solver: None, binding_overlay: None, zeq_proof: None, raw: Value::Null,
    });
    r.raw = raw;
    r
}

// ── CLI smoke test ────────────────────────────────────────────────────────
// This is gated behind `fn main()` so a single `rustc zeq_client.rs` (with the
// three crates above in scope) runs the Earth-Moon gravity test.

fn main() {
    let c = Client::new("");
    let res = c.compute("NM21", json!({
        "m1": 5.972e24f64, "m2": 7.342e22f64, "r": 3.844e8f64
    }));
    match res {
        Ok(r) => {
            println!("{}", "=".repeat(70));
            println!("NM21 — Earth-Moon gravitational force  (expected ~ 1.98e20 N)");
            println!("{}", "=".repeat(70));
            println!("value           = {:?}", r.value);
            println!("unit            = {:?}", r.unit);
            println!("uncertainty     = {:?}", r.uncertainty);
            println!("solver          = {:?}", r.solver);
            println!("binding_overlay = {:?}", r.binding_overlay);
            let proof = r.zeq_proof.unwrap_or_default();
            let preview: String = proof.chars().take(24).collect();
            println!("zeqProof        = {preview}...");
        }
        Err(e) => {
            eprintln!("error: {e}");
            std::process::exit(1);
        }
    }
}