Authentication
How to authenticate with the Etch API.
Overview
Etch uses wallet-based authentication: you sign a message with your Solana wallet and get a JWT. film.fun products calling Etch from their servers (Signal, Canon, Studio) can also send a product API key; see Product API keys.
Authentication Flow
Step 1: Request a Challenge
curl -X POST https://etch.film.fun/api/auth/challenge \
-H "Content-Type: application/json" \
-d '{ "wallet": "YOUR_WALLET_ADDRESS" }'Response:
{
"challenge": "Sign this message to authenticate with Etch:\n\nNonce: abc123\nTimestamp: 2026-02-15T12:00:00Z",
"nonce": "abc123",
"expiresAt": "2026-02-15T12:05:00Z"
}Step 2: Sign the Challenge
Sign the challenge message with your wallet. In JavaScript:
import { Connection, Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";
const message = new TextEncoder().encode(challenge);
const signature = nacl.sign.detached(message, wallet.secretKey);
const signatureBase58 = bs58.encode(signature);Step 3: Verify and Get Token
curl -X POST https://etch.film.fun/api/auth/verify \
-H "Content-Type: application/json" \
-d '{
"wallet": "YOUR_WALLET_ADDRESS",
"nonce": "abc123",
"signature": "BASE58_SIGNATURE"
}'Response:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-02-16T12:00:00Z",
"wallet": "YOUR_WALLET_ADDRESS"
}Step 4: Use the Token
Include the token in all subsequent requests:
curl https://etch.film.fun/api/launches \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Token Expiration
Tokens expire after 24 hours. To refresh:
- Request a new challenge
- Sign and verify again
Code Examples
TypeScript/JavaScript
import { Keypair } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";
async function authenticate(wallet: Keypair) {
// 1. Get challenge
const challengeRes = await fetch("https://etch.film.fun/api/auth/challenge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet: wallet.publicKey.toBase58() }),
});
const { challenge, nonce } = await challengeRes.json();
// 2. Sign challenge
const message = new TextEncoder().encode(challenge);
const signature = nacl.sign.detached(message, wallet.secretKey);
const signatureBase58 = bs58.encode(signature);
// 3. Verify and get token
const verifyRes = await fetch("https://etch.film.fun/api/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
wallet: wallet.publicKey.toBase58(),
nonce,
signature: signatureBase58,
}),
});
const { token } = await verifyRes.json();
return token;
}
// Use the token
const token = await authenticate(myWallet);
const launches = await fetch("https://etch.film.fun/api/launches", {
headers: { Authorization: `Bearer ${token}` },
});Python
import requests
from solana.keypair import Keypair
import nacl.signing
import base58
def authenticate(wallet: Keypair) -> str:
# 1. Get challenge
challenge_res = requests.post(
"https://etch.film.fun/api/auth/challenge",
json={"wallet": str(wallet.public_key)}
)
data = challenge_res.json()
challenge = data["challenge"]
nonce = data["nonce"]
# 2. Sign challenge
message = challenge.encode()
signed = wallet.sign(message)
signature = base58.b58encode(signed.signature).decode()
# 3. Verify
verify_res = requests.post(
"https://etch.film.fun/api/auth/verify",
json={
"wallet": str(wallet.public_key),
"nonce": nonce,
"signature": signature
}
)
return verify_res.json()["token"]Product API keys
Server-to-server calls from film.fun products may add x-etch-api-key: etk_<product>_…. Keys are issued by the Etch operator (scripts/product-api-key.mjs); only their SHA-256 is stored. Keep them in server env, never in a browser bundle.
| Scope | What it allows |
|---|---|
proxy | The request also carries the end user’s JWT (e.g. Signal’s /api/etch proxy). Etch trusts x-etch-client-ip for that user’s rate-limit bucket instead of putting every user behind the proxy’s one IP. |
launch | POST /api/tokens/launch without a JWT, acting as the wallet the key was issued for (e.g. Canon’s operator). |
With any valid key:
- the launch’s
productis the key’s product (a body naming another product gets403 product_mismatch), so product webhooks andexternalProjectIdlinks can’t be spoofed by other wallets; - an invalid, revoked or expired key is
401 invalid_api_key, never silently ignored.
Without the header nothing changes. An operator can set LAUNCH_REQUIRE_PRODUCT_KEY=true to refuse launches that don’t come through a product (403 product_key_required).
Security Best Practices
- Never share your token — Treat it like a password
- Use HTTPS — All API requests must use HTTPS
- Rotate tokens — Re-authenticate periodically
- Store securely — Don’t commit tokens to version control