> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Gate a refund with a verified decision

> Store a refund policy, ask NSR whether an agent may refund order_10042, read approved / denied / refused with the cited rules, verify the proof without trusting the engine, and wire the fail-closed path into the agent.

A support agent wants to refund **order\_10042** for **\$84.00**. Before it touches the payment provider,
it asks NSR, which answers with one of three verdicts — `approved`, `denied` or `refused` — the rules it
cited, a replayable derivation and, on approval, a proof bundle you can check yourself. This guide builds
that gate end to end; the [Verified Decisions API](/stateset-nsr-decisions) page is the contract behind it.

## Before you start

|          |                                                                                                                                                |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Base URL | `https://api.nsr.stateset.com`                                                                                                                 |
| Auth     | `X-API-Key: nsr_…` on every call. An org-bound key identifies your organization; send `X-Org-ID: <org>` only when your key is not bound to one |
| Scopes   | `write` for rules, decisions and proof verification; `read` for `GET /v1/decisions/{id}`                                                       |

The examples assume `NSR_API=https://api.nsr.stateset.com` and `NSR_API_KEY=nsr_your_key` are exported in your shell.

<Note>
  Two encodings are easy to mix up. **Stored** rules (`/api/v1/rules`) use `head_predicate`,
  `head_args` and `body` atoms keyed by `predicate`. **Request-scoped** facts and rules on
  `/v1/decisions` use `{ "predicate": { "name", "args" } }` envelopes and `if` / `then` atoms keyed by
  `name`. A string starting with `?` is a variable; everything else is a value.
</Note>

<Steps>
  <Step title="Lint the policy, then store it">
    Three Horn-clause rules make the refund policy: a permit with a negation-as-failure guard, a deny,
    and a review threshold using the evaluable builtin `gt`. Lint each one first — `POST /api/v1/rules/lint`
    takes the same body as a create, answers `{ "diagnostics": [...], "has_errors": bool }` and installs
    nothing. `RULE007` (head variable unbound in the body) and `RULE008` (builtin as head) are the errors to expect.

    ```bash theme={null}
    curl --request POST "$NSR_API/api/v1/rules/lint" \
      --header "X-API-Key: $NSR_API_KEY" --header "Content-Type: application/json" \
      --data @refund_ok.json
    ```

    Then install all three with `POST /api/v1/rules/batch`. Each rule is created independently — a
    failure does not roll back the others — and a `name` that already exists is replaced, not duplicated.

    ```bash theme={null}
    curl --request POST "$NSR_API/api/v1/rules/batch" \
      --header "X-API-Key: $NSR_API_KEY" \
      --header "Content-Type: application/json" \
      --header "Idempotency-Key: refund-policy-v1" \
      --data '{ "rules": [
        { "name": "refund_ok", "effect": "permit", "head_predicate": "may_refund", "head_args": ["?o"],
          "body": [ { "predicate": "return_received", "args": ["?o"] },
                    { "predicate": "inspection_passed", "args": ["?o"] },
                    { "predicate": "fraud_flagged", "args": ["?o", "?r"], "negated": true } ] },
        { "name": "fraud_blocks_refund", "effect": "deny", "head_predicate": "refund_blocked", "head_args": ["?o"],
          "body": [ { "predicate": "fraud_flagged", "args": ["?o", "?r"] } ] },
        { "name": "large_refund_review", "effect": "review", "head_predicate": "refund_requires_review", "head_args": ["?o"],
          "body": [ { "predicate": "order_amount", "args": ["?o", "?v"] },
                    { "predicate": "gt", "args": ["?v", "500"] } ] }
      ] }'
    ```

    ```json Response theme={null}
    { "created": 3, "failed": 0, "rules": [ { "id": "rule_01j…", "name": "refund_ok", "status": "created" }, "…" ], "errors": [] }
    ```

    <Note>
      The head names also follow the conventions the gate recognises lexically (`may_*`, `*_blocked`,
      `*_requires_review`), so the verdict is the same whether it reads the declared `effect` or the
      head. Deny wins over permit; a fired review rule yields `refused` with `requires_human_review: true`.
    </Note>
  </Step>

  <Step title="Ask for the decision">
    `POST /v1/decisions`. The stored policy is pulled in by `hydrate_org_context: true` (the default);
    the live facts about this order travel in the request. Declaring an `authorization_goal` is what makes
    an approval carry a `verifiable_bundle`; `external_ref` lets you report the real outcome later by your own order id.

    ```bash theme={null}
    curl --request POST "$NSR_API/v1/decisions" \
      --header "X-API-Key: $NSR_API_KEY" \
      --header "Content-Type: application/json" \
      --header "Idempotency-Key: refund-order_10042-attempt-1" \
      --data '{
        "query": "Can order_10042 be refunded?",
        "action": "issue_refund",
        "mode": "safe",
        "facts": [
          { "predicate": { "name": "return_received",   "args": ["order_10042"] },     "confidence": 1.0, "source": "wms" },
          { "predicate": { "name": "inspection_passed", "args": ["order_10042"] },     "confidence": 1.0, "source": "wms" },
          { "predicate": { "name": "order_amount",      "args": ["order_10042", 84] }, "confidence": 1.0, "source": "oms" }
        ],
        "authorization_goal": { "name": "may_refund", "args": ["order_10042"] },
        "external_ref": "order_10042",
        "hydrate_org_context": true
      }'
    ```

    Decisions default to `mode: "safe"`, the most thorough verification, because they are accountable outputs.
    The `Idempotency-Key` makes a retry within 24 hours replay the stored response — same `decision_id`,
    not re-decided, not re-billed — with `idempotent-replayed: true`.

    ```json Response (approved) theme={null}
    {
      "decision_id": "dec_01j9k…", "decision": "approved", "confidence": 0.94,
      "rationale": "…", "evaluated_goal": "may_refund(order_10042)",
      "plain_explanation": "Approved to issue a refund for order_10042: the return for order_10042 was received and order_10042 passed inspection.",
      "action": { "name": "issue_refund", "ready": true, "rule_name": "refund_ok", "rule_conclusion": "may_refund(order_10042)", "confidence": 0.94, "reason": "…" },
      "proof": {
        "cited_rules": ["refund_ok"],
        "derivation": {
          "steps": [ { "rule": "refund_ok", "premises": ["return_received(order_10042)", "inspection_passed(order_10042)", "not fraud_flagged(order_10042, ?r)"], "conclusions": ["may_refund(order_10042)"] } ],
          "authorizing_predicate": "may_refund(order_10042)", "replayable": true, "org_facts_used": [],
          "cited_org_rules": [ { "name": "refund_ok", "conditions": ["return_received(?o)", "inspection_passed(?o)", "not fraud_flagged(?o, ?r)"], "conclusions": ["may_refund(?o)"] } ]
        },
        "symbolic_steps": 2, "neural_steps": 0, "planner_steps": 1, "proof_score": 0.9, "proof_status": "grounded",
        "request_policy_hash": "sha256:3f9a…"
      },
      "grounding": { "status": "grounded", "grounded_entity_count": 1, "grounded_target_count": 1, "cited_rule_count": 1 },
      "gss_machine": { "authority": "nsr_machine_gss", "seed_grounded": true, "safety_gate_triggered": false, "escalation_triggered": false, "machine_state_version": 42, "…": "…" },
      "usage": { "billable": true, "outcome_cost_microdollars": 5000, "billing_tier": "standard", "elapsed_ms": 212, "iterations": 3, "max_depth_reached": 2 },
      "verifiable_bundle": { "facts": ["…"], "rules": ["…"], "proof": { "goal": "…", "steps": ["…"], "confidence": 1.0 }, "org_id": "org_your_org" }
    }
    ```

    The headers carry the verdict and charge too (`x-nsr-decision: approved`,
    `x-nsr-outcome-billable: true`, `x-nsr-outcome-cost-micros: 5000`), so a proxy can log the outcome
    without parsing the body. Keep `decision_id` and `proof.request_policy_hash` with the refund:
    `GET /v1/decisions/{id}` returns the record — verdict, cited rule ids, the replayable request
    snapshot, the same policy hash and, once reported, the real `outcome` — while it is inside the
    retained window; a 404 means it rotated out of the bounded log, and billing truth lives in metering.
  </Step>

  <Step title="Read the three verdicts">
    Change the facts and the verdict follows. Add `fraud_flagged(order_10042, chargeback_history)`
    and the deny rule fires: `"decision": "denied"`, `"cited_rules": ["fraud_blocks_refund"]`, with a
    `plain_explanation` that names the flag. Deny wins even though the permit rule's other premises hold.

    Drop `inspection_passed` and the engine cannot prove the goal. It does not guess — it refuses and
    names the exact premise that would unblock the proof, in the shape you can echo straight back:

    ```json Response (refused — evidence gap) theme={null}
    { "decision": "refused", "confidence": 0.0,
      "refusal": {
        "reason": "no cited policy rule authorized issue_refund for order_10042",
        "requires_human_review": false,
        "missing_facts": [ {
          "predicate": "inspection_passed", "args": ["order_10042"], "needed_by_rule": "refund_ok",
          "question": "Has order_10042 passed inspection?",
          "resolves_with": { "predicate": { "name": "inspection_passed", "args": ["order_10042"] } } } ] },
      "proof": { "cited_rules": [], "…": "…" } }
    ```

    Make the order \$620.00 instead (`"args": ["order_10042", 620]`) and the review rule fires — the
    other kind of refusal, with **no** `missing_facts`, because a review or safety refusal must not
    invite resubmission:

    ```json Response (refused — human review) theme={null}
    { "decision": "refused",
      "refusal": { "reason": "policy rule large_refund_review requires human review", "requires_human_review": true, "missing_facts": [] },
      "proof": { "cited_rules": ["large_refund_review"], "…": "…" } }
    ```

    <Warning>
      `refused` is a successful, safe outcome — never a soft yes, never something to retry blindly. With
      `missing_facts`, look each premise up in your own systems and resubmit. With `requires_human_review:
                true`, escalate; the decision also emits a `decision.review_required` webhook. Refusals are metered
      like any other outcome, so a refusal is not a free probe.
    </Warning>
  </Step>

  <Step title="Verify the proof without trusting the engine">
    Save the approval's `verifiable_bundle` exactly as returned and POST it to `POST /v1/proofs/verify`.
    The handler is stateless — no tenant state, no database. The bundle carries its own `facts`,
    `rules`, `proof` and `org_id`, and the checker replays the derivation against nothing but those.

    ```bash theme={null}
    curl --request POST "$NSR_API/v1/decisions" … | jq '.verifiable_bundle' > bundle.json

    curl --request POST "$NSR_API/v1/proofs/verify" \
      --header "X-API-Key: $NSR_API_KEY" \
      --header "Content-Type: application/json" \
      --data @bundle.json
    ```

    ```json Response theme={null}
    { "verified": true }
    ```

    It is a real check. Delete the `inspection_passed` triple from `facts` in `bundle.json` and send
    it again — still a 200, because an unsound proof is a valid *answer*, not a client error:

    ```json Response theme={null}
    { "verified": false, "reason": "proof step 1: cited fact inspection_passed(order_10042) is not in the bundle", "failed_step": 1 }
    ```

    <Note>
      Since v0.9.4 the checker verifies **entailment, not citation shape**: every cited fact is unified with
      the atom the rule needs, heads and bodies are replayed, builtins are evaluated, and uncertified negation
      fails closed. The same kernel ships as the dependency-light `nsr-proof-core` crate for offline
      verification; upgrade it with the server, since an older verifier accepts proofs the server now rejects.
    </Note>

    `verifiable_bundle` is present only when you declared an `authorization_goal` **and** the engine
    reproduced the proof; it is skipped above 64 facts or 64 rules across request and org scope, and for
    facts of arity three or more. The verdict stands on its own derivation either way.
  </Step>

  <Step title="Wire the gate into the agent, fail closed">
    The agent may call the refund tool **only** after an `approved` verdict whose bundle verifies. Every
    other path — denied, refused, an HTTP error, a timeout, an unreachable engine — ends without a side effect.

    ```typescript theme={null}
    const NSR = "https://api.nsr.stateset.com";
    const headers = { "X-API-Key": process.env.NSR_API_KEY!, "Content-Type": "application/json" };

    async function gateRefund(orderId: string, facts: Fact[], attempt = 1): Promise<GateResult> {
      let res: Response;
      try {
        res = await fetch(`${NSR}/v1/decisions`, {
          method: "POST", headers: { ...headers, "Idempotency-Key": `refund-${orderId}-attempt-${attempt}` },
          signal: AbortSignal.timeout(8_000),
          body: JSON.stringify({ query: `Can ${orderId} be refunded?`, action: "issue_refund", mode: "safe", facts,
                                 authorization_goal: { name: "may_refund", args: [orderId] }, external_ref: orderId }),
        });
      } catch { return { act: false, route: "escalate", why: "nsr_unreachable" }; }        // no engine, no refund
      if (res.status === 402) return { act: false, route: "escalate", why: "outcome_quota_exhausted" };
      if (res.status === 429 || res.status >= 500) return { act: false, route: "retry_later", why: `http_${res.status}` };
      if (!res.ok) return { act: false, route: "escalate", why: `http_${res.status}` };

      const d = await res.json();
      const id = d.decision_id;
      if (d.decision === "denied") return { act: false, route: "tell_customer", why: d.plain_explanation, id };
      if (d.decision === "refused") {
        if (d.refusal?.requires_human_review || !d.refusal?.missing_facts?.length || attempt > 1)
          return { act: false, route: "escalate", why: d.refusal?.reason, id };
        const more = await lookUpFacts(d.refusal.missing_facts);       // your systems answer each `question`
        if (!more.length) return { act: false, route: "escalate", why: "missing_facts_unresolved", id };
        return gateRefund(orderId, [...facts, ...more], attempt + 1);    // resubmit once, using `resolves_with` shapes
      }
      // approved — act only on a proof you checked yourself
      if (!d.verifiable_bundle) return { act: false, route: "escalate", why: "approved_without_bundle", id };
      const v = await fetch(`${NSR}/v1/proofs/verify`, { method: "POST", headers, body: JSON.stringify(d.verifiable_bundle) });
      const verdict = v.ok ? await v.json() : { verified: false };
      if (!verdict.verified) return { act: false, route: "escalate", why: verdict.reason ?? "proof_unverified", id };
      return { act: true, route: "refund", id, policyHash: d.proof.request_policy_hash };
    }
    ```

    Three rules the code encodes:

    * **Absence of a verdict is not a verdict.** A timeout, a connection error, a `5xx`, or the `503` the
      engine returns when it cannot complete machine inference all mean *do not refund*. `5xx` responses
      are never cached under an idempotency key, so a later retry reaches a healthy backend.
    * **Resubmit at most once**, only for an evidence-gap refusal, only with facts your own systems
      supplied. Never lower `confidence_threshold` to turn a refusal into an approval.
    * **Log `decision_id` and `request_policy_hash` next to the refund**, then close the loop from the
      system that learns the truth, keyed by the order id it already holds:

    ```bash theme={null}
    curl --request POST "$NSR_API/v1/decisions/outcome-by-ref" \
      --header "X-API-Key: $NSR_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{ "external_ref": "order_10042", "outcome": "honored" }'
    ```

    `honored`, `reversed`, `overridden` or `escalated` feed `GET /v1/decisions/calibration`, which
    tells you whether a reported `0.94` holds up 94% of the time — and therefore where an auto-approve
    threshold can defensibly sit.
  </Step>
</Steps>

## What you built

| Piece                                                         | Where it lives                                         | What it does                                                                                                          |
| ------------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| Refund policy — permit, deny, review                          | Org knowledge base, via `POST /api/v1/rules/batch`     | Judges every hydrated decision; replaced atomically by name on re-upload                                              |
| Decision request with `authorization_goal` and `external_ref` | `POST /v1/decisions`                                   | Returns `approved` / `denied` / `refused` with cited rules, a replayable derivation, a policy hash and a proof bundle |
| Independent proof check                                       | `POST /v1/proofs/verify` (or `nsr-proof-core` offline) | Confirms the approval against nothing but the bundle; catches a tampered or unrelated citation                        |
| Fail-closed agent gate                                        | Your agent                                             | Acts only on a verified approval; treats refusal, denial, quota, timeout and an unreachable engine as *do not act*    |
| Outcome loop                                                  | `POST /v1/decisions/outcome-by-ref`                    | Turns reported confidence into a measured calibration curve                                                           |

## Troubleshooting

<AccordionGroup>
  <Accordion title="The decision is refused although I supplied every fact">
    Check the encoding — facts are `{ "predicate": { "name", "args" } }` envelopes — and that the subject
    token matches in the facts and in `authorization_goal`. A threshold needs the raw number
    (`["order_10042", 84]`), not a precomputed boolean. If `missing_facts` is empty and
    `requires_human_review` is true, a review rule or the GSS safety gate fired; more facts will not change that.
  </Accordion>

  <Accordion title="The approval has no verifiable_bundle, or verify says false on an untouched one">
    No bundle: you did not declare `authorization_goal`, a fact had three or more arguments, or the
    combined request-plus-org facts or rules exceeded 64. `verified: false`: send the bundle
    byte-for-byte, with no re-serialisation or wrapper, and make sure an offline `nsr-proof-core` is at
    least v0.9.4.
  </Accordion>

  <Accordion title="402 on POST /v1/decisions, or 404 on GET /v1/decisions/{id}">
    402 means the organization's outcome quota is exhausted — refusals count too; watch
    `x-nsr-outcome-remaining` and the billing pages in the [NSR console](/stateset-nsr-console). 404 on a
    recent decision means the bounded log rotated it out; report outcomes as they happen and use
    `GET /v1/decisions/export` for a durable JSONL trail.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Verified Decisions API" icon="gavel" href="/stateset-nsr-decisions">
    The full contract — rule effects, negation, the outcome gate, soundness invariants and the v0.9.4 verification changes.
  </Card>

  <Card title="Agent Gate" icon="shield-halved" href="/stateset-nsr-agent-gate">
    The same fail-closed pattern as an MCP gateway, so the agent never holds an unguarded refund tool.
  </Card>

  <Card title="NSR MCP server" icon="plug" href="/stateset-nsr-mcp">
    `nsr_decide`, `nsr_verify_proof` and `nsr_record_outcome_by_ref` as agent tools.
  </Card>

  <Card title="Endpoint reference" icon="book" href="/api-reference/nsr/verified-decisions/v1-proofs-verify-create">
    Generated pages for [`/v1/decisions`](/api-reference/nsr/verified-decisions/v1-decisions-create), [`/api/v1/rules`](/api-reference/nsr/rules/rules-create) and `/v1/proofs/verify`, with playgrounds.
  </Card>
</CardGroup>
