Deploy automated services that authenticate with Ed25519 signatures, store data in custom tables, and manage files through object storage — all at $1 per 1,000 users/month.
Bots authenticate using Ed25519 key pairs instead of email and password. Your bot generates a key pair, registers its public key once, then signs a timestamp-based message on each auth request. No passwords, no cookies, no browser required.
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import base64
private_key = Ed25519PrivateKey.generate()
pub_bytes = private_key.public_key().public_bytes_raw()
public_key_b64 = base64.b64encode(pub_bytes).decode()
print(f"Public key: {public_key_b64}")
# Save the private key securely — you'll need it to authenticate
import nacl from "tweetnacl";
const keyPair = nacl.sign.keyPair();
const publicKeyB64 = Buffer.from(keyPair.publicKey).toString("base64");
console.log(`Public key: ${publicKeyB64}`);
// Save keyPair.secretKey securely
First, obtain a confirmed challenge from the confirmations API. Then register your bot's public key.
import httpx
resp = httpx.post(
"https://api.ezauth.org/v1/bot/signup",
headers={"X-Publishable-Key": "pk_..."},
json={
"challenge_id": challenge_id,
"public_key": public_key_b64,
},
)
bot_id = resp.json()["bot_id"]
print(f"Bot registered: {bot_id}")
const resp = await fetch("https://api.ezauth.org/v1/bot/signup", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Publishable-Key": "pk_...",
},
body: JSON.stringify({
challenge_id: challengeId,
public_key: publicKeyB64,
}),
});
const { bot_id } = await resp.json();
console.log(`Bot registered: ${bot_id}`);
curl -X POST https://api.ezauth.org/v1/bot/signup \
-H "Content-Type: application/json" \
-H "X-Publishable-Key: pk_..." \
-d '{
"challenge_id": "chal_abc123",
"public_key": "BASE64_ED25519_PUBLIC_KEY"
}'
# {"bot_id": "uuid-...", "public_key": "..."}
Sign a message containing your app ID, bot ID, and the current Unix timestamp. The server verifies the signature against your stored public key and returns a JWT session.
import time, base64
app_id = "your-app-uuid"
timestamp = int(time.time())
message = f"ezauth:bot_auth:{app_id}:{bot_id}:{timestamp}"
signature = private_key.sign(message.encode())
signature_b64 = base64.b64encode(signature).decode()
resp = httpx.post(
"https://api.ezauth.org/v1/bot/auth",
headers={"X-Publishable-Key": "pk_..."},
json={
"bot_id": bot_id,
"timestamp": timestamp,
"signature": signature_b64,
},
)
session = resp.json()
access_token = session["access_token"]
refresh_token = session["refresh_token"]
import nacl from "tweetnacl";
const appId = "your-app-uuid";
const timestamp = Math.floor(Date.now() / 1000);
const message = `ezauth:bot_auth:${appId}:${botId}:${timestamp}`;
const msgBytes = new TextEncoder().encode(message);
const signature = nacl.sign.detached(msgBytes, keyPair.secretKey);
const signatureB64 = Buffer.from(signature).toString("base64");
const resp = await fetch("https://api.ezauth.org/v1/bot/auth", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Publishable-Key": "pk_...",
},
body: JSON.stringify({
bot_id: botId,
timestamp,
signature: signatureB64,
}),
});
const { access_token, refresh_token } = await resp.json();
# Sign the message externally, then:
curl -X POST https://api.ezauth.org/v1/bot/auth \
-H "Content-Type: application/json" \
-H "X-Publishable-Key: pk_..." \
-d '{
"bot_id": "uuid-...",
"timestamp": 1709000000,
"signature": "BASE64_ED25519_SIGNATURE"
}'
# {"access_token": "eyJ...", "refresh_token": "...",
# "user_id": "...", "session_id": "..."}
Once authenticated, bots can read and write to custom tables and object storage using the session token — exactly like a regular user.
headers = {
"Authorization": f"Bearer {access_token}",
"X-Publishable-Key": "pk_...",
}
# Write a row to a custom table
httpx.post(
"https://api.ezauth.org/v1/tables/my-table/rows",
headers=headers,
json={"values": {"status": "running", "score": 42}},
)
# Upload a file to object storage
httpx.put(
"https://api.ezauth.org/v1/buckets/my-bucket/objects/report.json",
headers={**headers, "Content-Type": "application/json"},
content=json.dumps({"results": [1, 2, 3]}),
)
# Read it back
data = httpx.get(
"https://api.ezauth.org/v1/buckets/my-bucket/objects/report.json",
headers=headers,
)
const headers = {
Authorization: `Bearer ${access_token}`,
"X-Publishable-Key": "pk_...",
};
// Write a row to a custom table
await fetch("https://api.ezauth.org/v1/tables/my-table/rows", {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ values: { status: "running", score: 42 } }),
});
// Upload a file to object storage
await fetch("https://api.ezauth.org/v1/buckets/my-bucket/objects/report.json", {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ results: [1, 2, 3] }),
});
// Read it back
const data = await fetch(
"https://api.ezauth.org/v1/buckets/my-bucket/objects/report.json",
{ headers }
);
A complete bot that registers, authenticates, and stores data:
import base64, time, json, httpx
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
API = "https://api.ezauth.org"
APP_ID = "your-app-uuid"
PK = "pk_..."
# Generate keys (do this once, save the private key)
private_key = Ed25519PrivateKey.generate()
public_key_b64 = base64.b64encode(
private_key.public_key().public_bytes_raw()
).decode()
# Register (one-time setup)
signup = httpx.post(f"{API}/v1/bot/signup", headers={"X-Publishable-Key": PK}, json={
"challenge_id": "confirmed-challenge-id",
"public_key": public_key_b64,
}).json()
bot_id = signup["bot_id"]
# Authenticate (repeat when token expires)
ts = int(time.time())
msg = f"ezauth:bot_auth:{APP_ID}:{bot_id}:{ts}"
sig = base64.b64encode(private_key.sign(msg.encode())).decode()
session = httpx.post(f"{API}/v1/bot/auth", headers={"X-Publishable-Key": PK}, json={
"bot_id": bot_id, "timestamp": ts, "signature": sig,
}).json()
# Use the session
h = {"Authorization": f"Bearer {session['access_token']}", "X-Publishable-Key": PK}
# Insert a row
httpx.post(f"{API}/v1/tables/metrics/rows", headers=h, json={
"values": {"event": "deploy", "version": "1.0.3"}
})
# Upload a file
httpx.put(
f"{API}/v1/buckets/artifacts/objects/build.tar.gz",
headers={**h, "Content-Type": "application/gzip"},
content=open("build.tar.gz", "rb").read(),
)
import nacl from "tweetnacl";
const API = "https://api.ezauth.org";
const APP_ID = "your-app-uuid";
const PK = "pk_...";
// Generate keys (do this once, save the secret key)
const keyPair = nacl.sign.keyPair();
const publicKeyB64 = Buffer.from(keyPair.publicKey).toString("base64");
// Register (one-time setup)
const signup = await fetch(`${API}/v1/bot/signup`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Publishable-Key": PK },
body: JSON.stringify({
challenge_id: "confirmed-challenge-id",
public_key: publicKeyB64,
}),
}).then((r) => r.json());
const botId = signup.bot_id;
// Authenticate (repeat when token expires)
const ts = Math.floor(Date.now() / 1000);
const msg = `ezauth:bot_auth:${APP_ID}:${botId}:${ts}`;
const sig = nacl.sign.detached(new TextEncoder().encode(msg), keyPair.secretKey);
const sigB64 = Buffer.from(sig).toString("base64");
const session = await fetch(`${API}/v1/bot/auth`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-Publishable-Key": PK },
body: JSON.stringify({ bot_id: botId, timestamp: ts, signature: sigB64 }),
}).then((r) => r.json());
// Use the session
const h = {
Authorization: `Bearer ${session.access_token}`,
"X-Publishable-Key": PK,
};
// Insert a row
await fetch(`${API}/v1/tables/metrics/rows`, {
method: "POST",
headers: { ...h, "Content-Type": "application/json" },
body: JSON.stringify({ values: { event: "deploy", version: "1.0.3" } }),
});
// Upload a file
await fetch(`${API}/v1/buckets/artifacts/objects/build.tar.gz`, {
method: "PUT",
headers: { ...h, "Content-Type": "application/gzip" },
body: await Bun.file("build.tar.gz").arrayBuffer(),
});
Signature message format:
ezauth:bot_auth:{app_id}:{bot_id}:{unix_timestamp}
Endpoints:
POST /v1/bot/signup Register a new bot (publishable key)
POST /v1/bot/auth Authenticate and get a session (publishable key)
POST /v1/token/refresh Refresh an expired access token
GET /v1/tables List custom tables (session)
POST /v1/tables/{id}/rows Insert a row (session)
GET /v1/tables/{id}/rows Query rows (session)
PUT /v1/buckets/{id}/objects/{k} Upload an object (session)
GET /v1/buckets/{id}/objects/{k} Download an object (session)
Limits: