Skip to content
Tenbri pangolinTenbri

Record a refund

A support agent wants to refund a charge, and a person, Alice, approves it first. You will record three entries: Alice’s approval, the agent’s intent that cites the approval by its entry hash and names the policy and rule it relied on, and the outcome once the refund ran. Then you read them back and verify the stream against its pinned fingerprint. Pick a language once: every tab on the page follows it.

  • A shell with jq and openssl (for the CLI and REST tabs).
  • The tool for your language:
Terminal window
cargo install tenbri-cli --version 0.1.0-beta.1 --locked

Set these in every terminal you use. Keys live in $TENBRI_KEYS, never inside the stream; this recipe keeps them in ./keys. The digest key keys the HMAC of each input the agent read, so a guessed policy text cannot be confirmed from the log without it.

Terminal window
export TENBRI_KEYS=./keys
export TENBRI_DIGEST_KEY='a 32-byte secret, kept apart....'
  1. Create the stream, then record the approval, the intent and the outcome.

    An approval needs a principal (the approver) and an action (what is approved). The intent cites it as {"by": <approver>, "entry": <entry hash>}, where the entry hash is SHA-256 of a zero byte followed by the stored line (canonical JSON), as sha256:<hex>. The intent is durable before the refund runs; the outcome closes it by id.

    Terminal window
    tenbri init audit/support
    created stream tnb1:4282909ee7474c3eeeb243ba852f77da at audit/support
    stream key fingerprint (pin it): sha256:4282909ee7474c3eeeb243ba852f77da5b1f10dc68e6a83c4b6b26ada3749123
    keys: ./keys/tnb1-4282909ee7474c3eeeb243ba852f77da
    verify with: tenbri verify audit/support --key sha256:4282909ee7474c3eeeb243ba852f77da5b1f10dc68e6a83c4b6b26ada3749123

    init writes two key entries (seq 1 and 2): the stream key and the first session key. tenbri append reads a draft on stdin and prints the entry as stored.

    Terminal window
    APPROVAL=$(tenbri append audit/support <<'EOF'
    {
    "kind": "approval",
    "principal": {"kind": "person", "id": "alice"},
    "action": {"type": "issue_refund", "target": "stripe:charge/ch_3PqX"}
    }
    EOF
    )
    APPROVAL_HASH="sha256:$(printf '\0%s' "$APPROVAL" | shasum -a 256 | cut -d' ' -f1)"
    POLICY_DIGEST="hmac-sha256:$(printf '%s' 'refunds under 100 EUR are automatic' \
    | openssl dgst -sha256 -hmac "$TENBRI_DIGEST_KEY" -r | cut -d' ' -f1)"
    INTENT=$(tenbri append audit/support <<EOF
    {
    "kind": "decision",
    "phase": "intent",
    "source": "sdk",
    "principal": {"kind": "agent", "id": "support-assistant"},
    "action": {"type": "issue_refund", "target": "stripe:charge/ch_3PqX"},
    "inputs": [{"ref": "policy:refund", "version": "14", "digest": "$POLICY_DIGEST"}],
    "rule": {"ref": "rule:refund-under-100", "version": "3", "result": "allow"},
    "approval": {"by": {"kind": "person", "id": "alice"}, "entry": "$APPROVAL_HASH"}
    }
    EOF
    )
    echo "$INTENT"
    {"action":{"target":"stripe:charge/ch_3PqX","type":"issue_refund"},"approval":{"by":{"id":"alice","kind":"person"},"entry":"sha256:2cd7ac6d…"},"id":"01M5CWDKWVDZDN2HEZRAJZAF96","inputs":[{"digest":"hmac-sha256:100d69e9…","ref":"policy:refund","version":"14"}],"kind":"decision","phase":"intent","prev":"sha256:2cd7ac6d…","principal":{"id":"support-assistant","kind":"agent"},"rule":{"ref":"rule:refund-under-100","result":"allow","version":"3"},"seq":4,"sig":"ed25519:a8b663a6…","signer":"sess:2026-10-20:63214e562e83","source":"sdk","stream":"tnb1:4282909ee7474c3eeeb243ba852f77da","ts":"2026-10-20T14:46:03.931Z","v":1}

    Run the refund, then record the outcome:

    Terminal window
    INTENT_ID=$(echo "$INTENT" | jq -r .id)
    tenbri append audit/support <<EOF
    {
    "kind": "decision",
    "phase": "outcome",
    "source": "sdk",
    "intent": "$INTENT_ID",
    "status": "ok",
    "principal": {"kind": "agent", "id": "support-assistant"},
    "action": {"type": "issue_refund", "target": "stripe:charge/ch_3PqX"}
    }
    EOF
    {"action":{"target":"stripe:charge/ch_3PqX","type":"issue_refund"},"id":"01M5CWDKXS5350P7C6Z68AH64F","intent":"01M5CWDKWVDZDN2HEZRAJZAF96","kind":"decision","phase":"outcome","prev":"sha256:ace27fa3…","principal":{"id":"support-assistant","kind":"agent"},"seq":5,"sig":"ed25519:9f617ebc…","signer":"sess:2026-10-20:63214e562e83","source":"sdk","status":"ok","stream":"tnb1:4282909ee7474c3eeeb243ba852f77da","ts":"2026-10-20T14:46:03.961Z","v":1}

    In every tab, the intent’s prev equals the approval’s entry hash: the approval is the entry just before it, so the hash you computed is the one the chain uses. An approval.entry that does not name an earlier approval entry is refused with bad_approval.

  2. Read the entries back and verify.

    Pin the fingerprint the stream printed when you created it. Reads then check the stream against it, and each match carries its verification status.

    --verify checks the answers against the stream itself, even when a query index exists.

    Terminal window
    PIN=sha256:4282909ee7474c3eeeb243ba852f77da5b1f10dc68e6a83c4b6b26ada3749123
    tenbri query audit/support --action issue_refund --key $PIN --verify \
    | jq -c '{verification, seq, kind: .entry.kind, phase: .entry.phase, status: .entry.status, approved_by: .entry.approval.by.id}'
    tenbri verify audit/support --key $PIN
    {"verification":"verified","seq":3,"kind":"approval","phase":null,"status":null,"approved_by":null}
    {"verification":"verified","seq":4,"kind":"decision","phase":"intent","status":null,"approved_by":"alice"}
    {"verification":"verified","seq":5,"kind":"decision","phase":"outcome","status":"ok","approved_by":null}
    verified 5 entries, stream tnb1:4282909ee7474c3eeeb243ba852f77da, pinned to sha256:4282909ee7474c3eeeb243ba852f77da5b1f10dc68e6a83c4b6b26ada3749123

    tenbri tail audit/support -n 3 --no-follow prints the same entries as stored.

You wrote three signed entries that chain to each other. The intent cites the approval by its entry hash and pins the policy version and rule it relied on, so none of them can be swapped or edited without breaking verification. The outcome names its intent by id, which closes it: an intent without an outcome is reconciled with status: unknown by a later writer after an hour.