Receipts and verification
Every answer is signed and chained into your log. The SDK checks each one before it acts, and keeps them as evidence.
What gets signed
- The decision. reTrade’s answer is a compact JWS, signed with Ed25519 by that day’s decision key. It records the answer, the order as approved, the SHA-256 of your exact request, the rules in force, reTrade’s clock and the decision’s place in your log.
- The receipt. After the broker call, the SDK reports what happened (placed, rejected or withdrawn), and reTrade answers with a signed receipt.
- The log head. reTrade signs the latest position of your log, and the SDK keeps each head it receives.
Chained, so any change shows
Each record in your tenant’s log carries its number and the hash of the record before it (seq and prev_hash). Change, remove or reorder a record and the chain no longer matches: the log is tamper-evident.
reTrade signs with a new decision key each day (UTC). Every key stays on the root-signed key list, GET /v1/keys, so an old answer can always be checked again.
What the SDK checks
Before anything reaches your broker, the SDK checks every answer:
- It’s a well-formed JWS of type
retrade-decision, signed with EdDSA. - Its key is on the key list your pinned root key signed, for your mode’s environment, and was valid when the decision was made.
- The Ed25519 signature verifies.
- It’s for your mode, account and agent, and its
request_sha256is the SHA-256 of the bytes the SDK sent. - It was made within 60 seconds of your clock.
- Its order fields and requested quantity are your order’s, and an ALLOW or CUT lives 5 to 60 seconds.
An answer that fails any check is never placed: you get a local BLOCK with reason unverified, and nothing is reported. The status reTrade signs, with the stops in force and each account’s settings, is checked too (its signature, mode, log position and age) before the SDK uses it.
The root key is pinned: the SDK never fetches it. If the key list wasn’t signed by your root key, the client refuses to start, with VerificationFailed: bad signature on a retrade-keys document.
The claim kit
The SDK writes everything to a local SQLite file as it goes: claim-kit.sqlite3, in its state directory (retrade-state by default). It holds your exact request bytes, reTrade’s signed decisions, the rules each was decided under, your submission reports with reTrade’s receipts, and the signed log heads. Each record is saved before the SDK goes on, and a decision is marked used before the broker call. Both SDKs write the same layout.
It’s the evidence that makes a mistake insurable: what the agent asked, what reTrade answered and what reached the broker, which an insurer can review to underwrite the risk and assess a claim. Keep it for at least 90 days. Only enforce-mode decisions are that evidence. reTrade is not an insurer and pays no losses: whether a loss is covered is up to the insurer.
if result.decision_id is not None: # None only when the SDK decided on its own
evidence = rt.claim_kit(result.decision_id)
# {"format": "retrade-claim-kit/1", "decision_id": ..., "request": ...,
# "decision": ..., "policy": ..., "report": ..., "receipt": ..., "heads": [...]}if (result.decisionId !== null) { // null only when the SDK decided on its own
const evidence = rt.claimKit(result.decisionId);
// { format: "retrade-claim-kit/1", decision_id, request, decision,
// policy, report, receipt, heads }
}retrade claim-kit <decision id> --state-dir retrade-statenpx retrade claim-kit <decision id> --state-dir retrade-stateIn the export, request and policy are base64url bytes, so they survive exactly; decision and receipt are compact JWS documents. Anyone with the root public key can check them again against GET /v1/keys.
Reading your log
log reads your tenant’s log for your key’s mode, each record checked against the key list and chained to the one before it.
records = rt.log() # the whole log, from record 1
first20 = rt.log(from_seq=1, to_seq=20) # records 1 to 20
for record in first20:
print(record.seq, record.kind, record.hash)const records = await rt.log(); // the whole log, from record 1
const first20 = await rt.log({ from: 1, to: 20 }); // records 1 to 20
for (const record of first20) console.log(record.seq, record.kind, record.hash);reTrade sends the log a page at a time, up to 1,000 records and 512 KiB a page, and log keeps asking until it reaches the end or the last record you asked for. The records are held in memory, so read a long log in ranges.