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

# Reputation

> Transfer, mint, and list reputation grants from the server

The `reputation` module moves reputation deliberately: **transfers** between two users, **mints** that create reputation from nothing (or destroy it), and reads of the grant history. It maps directly to the [Reputation Grants API](/api-reference/reputation-grants/create-reputation-grant).

Because the Node SDK authenticates with a **service key** (it acts as the whole project, not a single end user), `createGrant` takes an explicit `actingUserId` naming the sender. This is the *mediated transfer* path: it behaves exactly like a user-initiated grant — same bucket routing, same balance check — but it lets your backend enforce its own rules first (caps, cooldowns, daily limits, eligibility), which Sublay deliberately does not impose.

Two things a service key does **not** get you here:

* **Mediated transfers still respect the sender's balance.** Only `mintGrant` can create reputation or drive a bucket below zero.
* **Negative grants are not readable through `listGrants`.** The `amount > 0` filter applies to every caller, service keys included. Read them from the [dashboard grant-history route](/api-reference/reputation-grants/fetch-user-grant-history).

Requires the `reputation` bundle. A `chat-message` target additionally needs the `chat` bundle, and a `spaceId` needs the `spaces` bundle. `createGrant` and `mintGrant` validate `spaceId` identically: an ID naming no space is rejected with `404 reputation-grant/space-not-found` before anything moves, and naming a space on a project without `spaces` is rejected with `403 database/tables-not-available`.

***

### createGrant

Transfers reputation from one user to another — the amount leaves the sender's bucket and lands in the recipient's bucket for the same space. Nothing is created.

Mediated transfers skip the project's user-grants switch and ignore block relationships between the two parties.

```typescript theme={null}
const grant = await sublay.reputation.createGrant({
  actingUserId: "usr_abc123",
  recipientId: "usr_def456",
  amount: 50,
  spaceId: "spc_789",
  note: "Bounty payout",
  targetType: "comment",
  targetId: "cmt_012",
});
```

<ParamField body="actingUserId" type="string" required>
  The **sender** — the user reputation is debited from. Spelled `actingUserId` rather than `userId` because the body already names a target user (`recipientId`).
</ParamField>

<ParamField body="recipientId" type="string" required>The user credited. Must differ from `actingUserId`.</ParamField>

<ParamField body="amount" type="number" required>
  Whole number from `1` to `2147483647` (the signed 32-bit maximum). Never zero, negative, or fractional — use `mintGrant` to create or destroy reputation. It can never exceed what the sender holds in the source bucket.
</ParamField>

<ParamField body="spaceId" type="string | null">
  The bucket both legs move in. Omitted or `null` = the project-general bucket. Reputation never crosses between the general bucket and a space bucket. Validated: an unknown space is rejected with `404 reputation-grant/space-not-found`.
</ParamField>

<ParamField body="note" type="string | null">Free-text note. Trimmed, up to 2000 characters.</ParamField>
<ParamField body="metadata" type="object">Arbitrary key-value data. Up to 1 MB. Omit when unused.</ParamField>

<ParamField body="targetType" type="&#x22;entity&#x22; | &#x22;comment&#x22; | &#x22;chat-message&#x22;">
  What the grant is for. Supplied together with `targetId`. An annotation only — it never determines which bucket is used, and need not be authored by the recipient.
</ParamField>

<ParamField body="targetId" type="string">The rewarded record's ID. Supplied together with `targetType`, and must exist.</ParamField>

**Returns** — `Promise<ReputationGrant>` with `sourceType: "user"`.

Throws `409 reputation-grant/insufficient-reputation` when the sender's bucket is short — nothing moves; that is also the answer when the named space is real but the sender holds no funded bucket in it. A mediated transfer is still checked against the sender's suspension and, for a `chat-message` target, against the sender's active membership of that conversation. A grant is **not idempotent**: a retried call moves the points a second time.

***

### mintGrant

Creates reputation from nothing and credits a user. There is no sender and nothing is debited; the grant is written with a `null` `senderId` and `sourceType: "app"`.

A **negative** amount destroys reputation instead — the moderation clawback, decay sweep, or correction path. It is the only operation permitted to drive a bucket below zero, and it is entirely silent: no notification, no socket broadcast, and invisible on every public read surface.

```typescript theme={null}
// Contest payout
await sublay.reputation.mintGrant({
  recipientId: "usr_def456",
  amount: 500,
  note: "Hackathon winner",
});

// Reversing a mistaken grant
await sublay.reputation.mintGrant({
  recipientId: "usr_def456",
  amount: -500,
  note: "Reversal: duplicate payout",
});
```

<ParamField body="recipientId" type="string" required>The user credited — or, with a negative amount, debited.</ParamField>

<ParamField body="amount" type="number" required>
  Any **non-zero** whole number in the signed 32-bit range. Negatives destroy reputation.
</ParamField>

<ParamField body="spaceId" type="string | null">
  The bucket credited/debited. Omitted or `null` = the project-general bucket. Validated: an unknown space is rejected with `404 reputation-grant/space-not-found`.
</ParamField>

<ParamField body="note" type="string | null">Free-text note. Trimmed, up to 2000 characters.</ParamField>
<ParamField body="metadata" type="object">Arbitrary key-value data. Up to 1 MB. Omit when unused.</ParamField>
<ParamField body="targetType" type="&#x22;entity&#x22; | &#x22;comment&#x22; | &#x22;chat-message&#x22;">What the grant is for. Supplied together with `targetId`.</ParamField>
<ParamField body="targetId" type="string">The rewarded record's ID. Supplied together with `targetType`, and must exist.</ParamField>

**Returns** — `Promise<ReputationGrant>` with `sourceType: "app"`.

<Note>
  There is no counterpart in [`@sublay/js`](/v7/js-sdk/reputation) — minting is service/master-key only, and a user token can never reach the route.
</Note>

***

### listGrants

Lists grants. Exactly one filter shape per request — by recipient, by sender, or by target. The shapes are mutually exclusive and are not AND-ed; supplying none or combining two is a `400`.

```typescript theme={null}
const { data, pagination, summary } = await sublay.reputation.listGrants({
  targetType: "entity",
  targetId: "ent_abc123",
  include: "user",
  limit: 25,
});

console.log(summary?.total, "points from", summary?.count, "people");
```

<ParamField body="recipientId" type="string">What this user received.</ParamField>
<ParamField body="senderId" type="string">What this user sent.</ParamField>
<ParamField body="targetType" type="&#x22;entity&#x22; | &#x22;comment&#x22; | &#x22;chat-message&#x22;">Who rewarded this item. Supplied together with `targetId`.</ParamField>
<ParamField body="targetId" type="string">The rewarded record's ID. Supplied together with `targetType`.</ParamField>
<ParamField body="page" type="number">Page number (1-indexed). Defaults to `1`.</ParamField>
<ParamField body="limit" type="number">Page size. Defaults to `20`; the maximum is `100`. A larger value is rejected with `400 reputation-grant/invalid-query` rather than clamped.</ParamField>

<ParamField body="include" type="string">
  Comma-separated associations. Only `"user"` is supported — it hydrates both the sender and the recipient.
</ParamField>

<ParamField body="spaceReputation" type="object">
  Opt-in per-user space reputation on the hydrated users. `{ spaceId: "context" }` scores each user against that grant's own space. See [Reputation](/data-models/reputation#reading-space-scoped-reputation).
</ParamField>

**Returns** — `Promise<{ data: ReputationGrant[]; pagination; summary? }>`. The `summary` block (`{ total, count, viewerTotal }`) is returned **only** on the target filter shape.

<Note>
  **Grants on chat messages are private to their conversation — and a service key is exempt from that.** For a user token, every shape asks whether that user can see the underlying message and withholds the grant if they cannot. A service key answers to neither gate: the target shape returns its rows unconditionally, and the `recipientId` / `senderId` shapes apply no membership predicate to it at all, so a backend can enumerate every grant it minted, including grants on messages moderation has removed. It is the app's own backend, already trusted to mint grants and to read every conversation. It holds no viewer identity, so `summary.viewerTotal` is always `0`.
</Note>

<Warning>
  **A service key sees no negative grants here.** The `amount > 0` filter is baked into the query for every caller, so this is not the route for auditing moderation deductions — use [Fetch User Grant History](/api-reference/reputation-grants/fetch-user-grant-history), which requires `x-sublay-project-id` alongside your key.
</Warning>

***

## Reading totals inline

For per-item totals, don't page through this module — ask the read that already returns the item. Entities, comments and chat messages all accept a `"grants"` token in their `include` string (combine it with others, e.g. `"files,grants"`). Each item then carries `grants: { total, count, viewerTotal }`. See [GrantSummary](/data-models/reputation-grant#grantsummary).

## See Also

* [ReputationGrant data model](/data-models/reputation-grant)
* [Create Reputation Grant API](/api-reference/reputation-grants/create-reputation-grant)
* [Mint Reputation Grant API](/api-reference/reputation-grants/mint-reputation-grant)
* [JS SDK reputation module](/v7/js-sdk/reputation)
