Skip to content

Authentication

Every Onramp endpoint that touches a customer is authorized by a signature of the customer's Rubin key. There are no API keys: the address in the request is the customer, and only the holder of that address's key can create orders whose USDC is credited to it.

Public endpoints — GET /health and GET /v4/sbp/rates — need no signature. (If a customer's flow has ratesPerCustomer: true, sign GET /v4/sbp/rates as well.)

Headers

HeaderRequiredValue
x-onramp-addressalwaysCustomer address, bech32 rit1… (lowercase, 20-byte payload)
x-onramp-timestampalwaysUnix time in seconds
x-onramp-signaturealwaysSignature of the message below; encoding depends on the wallet type
x-onramp-wallet-typeno (default cosmos)cosmos, keplr, evm or session — see Signature schemes
x-onramp-pubkeyfor cosmos and keplrBase64 of the 33-byte compressed secp256k1 public key
Content-Typefor requests with a bodyapplication/json

Signed message

RITBIT_ONRAMP:{METHOD}:{path}:{bodyHash}:{timestamp}
PartRule
METHODUppercase HTTP method: GET or POST
pathFull request path including /v4 and without the query string, e.g. /v4/sbp/orders
bodyHashLowercase hex SHA-256 of the exact body bytes you send; empty string for GET and for requests without a body
timestampThe same value as x-onramp-timestamp

Examples:

RITBIT_ONRAMP:GET:/v4/sbp/customer::1791200000
RITBIT_ONRAMP:POST:/v4/sbp/orders:3b7a…e91c:1791200000

Validity window

The server accepts a timestamp within ±120 seconds of its own clock. There is no nonce: the same signature stays valid for the same method, path and body for the whole window, so a polling client may reuse one signature for repeated GET requests. Sign every POST afresh.

Signature schemes

x-onramp-wallet-typeWho signsUse it when
cosmosA secp256k1 key held by your backend or appRecommended for partner integrations — see below
evmAn EVM key or wallet, EIP-712 typed dataYou already hold EVM keys, or the user signs in an EVM wallet — see below
keplrKeplr wallet, ADR-36 signArbitraryThe end user connects a Keplr wallet
sessionOn-chain trading keysUsed by the rubin.trade web app; contact us before relying on it

cosmos scheme

  1. Build the signed message as above.
  2. Compute digest = SHA-256(message + ':ONRAMP":' + timestamp). Note the literal double quote after ONRAMP — the string is {message}:ONRAMP":{timestamp}.
  3. Sign digest with secp256k1. Send the signature as base64 of the 65-byte fixed-length form r ‖ s ‖ recovery.
  4. Send the compressed public key in x-onramp-pubkey. The server derives bech32("rit", ripemd160(sha256(pubkey))) and rejects the request if it differs from x-onramp-address.

A partner that serves users without their own Rubin wallets keeps one key per end user and uses that key's address as the customer. Each user then has their own verification, limits and order history, and the USDC is credited to their own address.

Example (TypeScript)

import { ripemd160, Secp256k1, sha256 } from '@cosmjs/crypto';
import { toBech32, toHex } from '@cosmjs/encoding';
 
const BASE_URL = 'https://onramp.testnet.rubin.trade';
const enc = new TextEncoder();
 
export async function onrampRequest(
  privateKey: Uint8Array,
  method: 'GET' | 'POST',
  pathWithQuery: string,
  body?: unknown,
) {
  const { pubkey } = await Secp256k1.makeKeypair(privateKey);
  const compressed = Secp256k1.compressPubkey(pubkey);
  const address = toBech32('rit', ripemd160(sha256(compressed)));
 
  const rawBody = body === undefined ? '' : JSON.stringify(body);
  const bodyHash = rawBody === '' ? '' : toHex(sha256(enc.encode(rawBody)));
  const timestamp = Math.floor(Date.now() / 1000);
  const path = pathWithQuery.split('?')[0];
 
  const message = `RITBIT_ONRAMP:${method}:${path}:${bodyHash}:${timestamp}`;
  const digest = sha256(enc.encode(`${message}:ONRAMP":${timestamp}`));
  const signature = await Secp256k1.createSignature(digest, privateKey);
 
  const res = await fetch(`${BASE_URL}${pathWithQuery}`, {
    method,
    headers: {
      'x-onramp-address': address,
      'x-onramp-timestamp': String(timestamp),
      'x-onramp-wallet-type': 'cosmos',
      'x-onramp-pubkey': Buffer.from(compressed).toString('base64'),
      'x-onramp-signature': Buffer.from(signature.toFixedLength()).toString('base64'),
      ...(rawBody === '' ? {} : { 'Content-Type': 'application/json' }),
    },
    body: rawBody === '' ? undefined : rawBody,
  });
 
  const data = await res.json();
  if (!res.ok) throw Object.assign(new Error(data.errors?.[0]?.msg), { status: res.status, data });
  return data;
}
 
// const customer = await onrampRequest(key, 'GET', '/v4/sbp/customer');

evm scheme

A Rubin address and an EVM address are two encodings of the same 20 bytes: rit1… is bech32("rit", bytes), 0x… is their hex. An EVM key therefore owns the Rubin address of the same bytes and can sign for it directly.

  1. Build the signed message as above.

  2. Sign EIP-712 typed data with exactly this domain and type — any difference changes the digest:

    FieldValue
    domain{ name: 'Rubin', version: '1' } — no chainId, no verifyingContract
    primary typeAccountAction(string action,string message,uint64 timestamp)
    actionONRAMP
    messageThe signed message from step 1
    timestampThe same value as x-onramp-timestamp
  3. Send the 65-byte signature as 0x-prefixed hex in x-onramp-signature. No x-onramp-pubkey: the server recovers the signer from the signature and compares its 20 bytes with x-onramp-address.

The domain names no chain, so a wallet signs it on whatever network it is connected to.

import { toBech32 } from '@cosmjs/encoding';
import { hexToBytes } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
 
const account = privateKeyToAccount(evmPrivateKey);
const address = toBech32('rit', hexToBytes(account.address)); // x-onramp-address
 
// message and timestamp built exactly as in the cosmos example above
const signature = await account.signTypedData({
  domain: { name: 'Rubin', version: '1' },
  types: {
    AccountAction: [
      { name: 'action', type: 'string' },
      { name: 'message', type: 'string' },
      { name: 'timestamp', type: 'uint64' },
    ],
  },
  primaryType: 'AccountAction',
  message: { action: 'ONRAMP', message, timestamp: BigInt(timestamp) },
});
 
// headers: x-onramp-address, x-onramp-timestamp,
//          x-onramp-wallet-type: 'evm', x-onramp-signature: signature

Authentication errors

HTTPMessageCause
401Missing onramp auth headersNo address, signature or timestamp header
400Invalid onramp auth addressThe address is not a valid rit1… address
401Invalid onramp auth timestampThe timestamp is not a number
400Timestamp is not within the valid range of 120 secondsClock skew or a stale signature
400Address … does not correspond to the pubkey provided …x-onramp-pubkey does not hash to the address
400Address … does not correspond to the signer …evm: the recovered signer is another address — usually a wrong domain, type or message
400Signature verification failedWrong message, wrong digest or a re-serialized body

Errors use the common error format.