Repository navigation
Gate Access Control
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
This document explains the gate-based access control implementation in the Sigvara SDK. A gate is an authorization layer that requires an agent to prove identity, demonstrate active standing, and meet a reputation threshold before being admitted. It also manages nonces to prevent replay attacks and ensure each challenge-response pair is used exactly once.
The gate separates authentication from authorization:
- Authentication proves that the caller controls the claimed agent identity for this specific request.
- Authorization checks whether that agent is active, holds at least the minimum stake in force now (alpha.9 trusts the stored status), and has sufficient matured reputation.
Gates are intended for off-chain services such as web applications, APIs, or tooling that need reputation-based access control without sending an on-chain transaction for every decision. Each admit does make read calls to the chain: one for the agent's registered key, then the status, the bond held now and the score, and the time the score was last written if you set maxScoreAgeSeconds.
This page describes the gate in 1.0.0-alpha.12, published to npm on 29 October 2026; the gate itself did not change since 1.0.0-alpha.10. These changes to the gate are new since 1.0.0-alpha.9 (listed in the changelog): the constructor refusing a threshold, ttlSeconds or maxScoreAgeSeconds that is not a usable number, the timeoutMs option and the 30 second read timeout, the status, the bond held now and the score being read together, the optional maxScoreAgeSeconds limit with its stale_score refusal, and the refusal of a challenge dated in the future.
The gate logic lives in the SDK package under packages/sdk. The relevant files are:
-
packages/sdk/src/gate.ts: the gate class, nonce store interface, and admission flow. -
packages/sdk/test/gate.test.ts: tests covering admission, refusal reasons, nonce handling, concurrency, store behavior, and configuration that would fail open. -
docs/reputation-model.md: background on how reputation scores are computed, matured, and consumed. -
packages/sdk/README.md: integration notes about audience configuration and shared nonce stores.
graph TB
Client["Client Application"] --> Gate["SigvaraGate<br/>packages/sdk/src/gate.ts"]
Gate --> Verifier["SigvaraVerifier<br/>packages/sdk/src/verifier.ts"]
Gate --> NonceStore["NonceStore<br/>In-memory or custom"]
Verifier --> Chain["On-chain Reputation Data"]
Diagram sources
Section sources
-
SigvaraGate: the main authorization component. It issues challenges, verifies signatures, tracks nonces, checks agent activity, reads matured reputation, and returns structured admission results. -
NonceStore: an interface for tracking spent nonces. It supports per-process memory storage by default and can be replaced with a distributed store for multi-instance deployments. -
AdmitResultandAdmitReason: typed responses describing whether access was granted and why it was refused. -
GateConfig: configuration for RPC connection, contract addresses, chain ID, reputation threshold, audience binding, challenge TTL, optional nonce store, the read timeouttimeoutMs, and the optional score-age limitmaxScoreAgeSeconds.
Key responsibilities:
- Issue a signed challenge containing a nonce, timestamp, audience, and DID.
- Verify the signature against the on-chain verifier.
- Prevent replay using a nonce store.
- Check whether the agent is active and still holds the minimum stake in force now (alpha.9 reads the stored status only).
- If
maxScoreAgeSecondsis set, refuse a score written longer ago than that, or never. - Compare the matured score against the configured threshold.
- Return a clear reason for every refusal.
- Refuse to be built with a
thresholdthat is not a finite number of 0 or more, or attlSecondsormaxScoreAgeSecondsthat is not a positive number.
Section sources
The gate implements a challenge-response protocol with explicit replay protection and reputation gating.
sequenceDiagram
participant App as "Application"
participant Gate as "SigvaraGate"
participant Store as "NonceStore"
participant Verifier as "SigvaraVerifier"
participant Chain as "Reputation Contract"
App->>Gate : "challenge(did)"
Gate-->>App : "Challenge {payload, nonce, expiresAt}"
App->>App : "Agent signs payload"
App->>Gate : "admit(did, challenge, signature)"
Gate->>Gate : "Parse signed payload"
Gate->>Verifier : "verifySignature(did, payload, signature, ttl, audience)"
Verifier-->>Gate : "verified or not"
alt "Invalid proof"
Gate-->>App : "{ok : false, reason : 'bad_proof'}"
else "Valid proof"
Gate->>Store : "consume(nonce, expiresAt)"
Store-->>Gate : "allowed or already spent"
alt "Already spent"
Gate-->>App : "{ok : false, reason : 'replayed'}"
else "Allowed"
Note over Gate,Verifier : isActive, hasMinimumStake and getTotalScore are sent together
Gate->>Verifier : "isActive(did) and hasMinimumStake(did)"
Verifier-->>Gate : "active and bonded, or not"
alt "Not active, or below the minimum stake"
Gate-->>App : "{ok : false, reason : 'not_active'}"
else "Active"
Gate->>Verifier : "getTotalScore(did)"
Verifier->>Chain : "read matured score"
Chain-->>Verifier : "score"
Verifier-->>Gate : "score"
alt "Score too old, only when maxScoreAgeSeconds is set"
Gate-->>App : "{ok : false, reason : 'stale_score', score}"
else "Fresh enough, or no limit set"
alt "Below threshold"
Gate-->>App : "{ok : false, reason : 'below_threshold', score}"
else "Meets threshold"
Gate-->>App : "{ok : true, score}"
end
end
end
end
end
Diagram sources
The gate evaluates requests in a strict order designed to avoid unnecessary chain reads and to provide actionable refusal reasons. The proof is checked first, the nonce is spent second, and the status, the bond held now and the score are read last, so a caller who cannot prove an identity cannot make the gate read any of them. Those reads do not depend on each other and are sent together, and the answer still names standing before score (alpha.9 reads the status, then the score, and does not read the bond). If you set maxScoreAgeSeconds, the time the score was last written is read in the same batch, and a score older than the limit, or never written, is refused as stale_score after standing and before the number.
flowchart TD
Start(["Request received"]) --> Parse["Parse signed payload"]
Parse --> ValidPayload{"Payload valid?"}
ValidPayload --> |No| BadProof["Return 'bad_proof'"]
ValidPayload --> |Yes| Verify["Verify signature and audience"]
Verify --> Verified{"Signature valid?"}
Verified --> |No| BadProof
Verified --> |Yes| Consume["Consume nonce"]
Consume --> Spent{"Nonce already spent?"}
Spent --> |Yes| Replayed["Return 'replayed'"]
Spent --> |No| Active["Read status, bond held now and matured score"]
Active --> IsActive{"Active, and bonded at the current minimum?"}
IsActive --> |No| NotActive["Return 'not_active'"]
IsActive --> |Yes| Fresh{"Score too old? Only if maxScoreAgeSeconds is set"}
Fresh --> |Yes| Stale["Return 'stale_score' with score"]
Fresh --> |No| Threshold{"Score >= threshold?"}
Threshold --> |No| BelowThreshold["Return 'below_threshold' with score"]
Threshold --> |Yes| Admit["Return ok with score"]
Diagram sources
Section sources
Nonces are central to preventing replay attacks. The gate owns nonce lifecycle because it issued the challenge and knows which nonces are outstanding.
Important behaviors:
- Each challenge contains a unique nonce inside the signed payload.
- The gate spends the nonce only after successful signature verification, so invalid proofs cannot burn nonces.
- The expiration time comes from the signed timestamp plus the gate’s TTL, not from a caller-supplied wrapper field.
- The default
MemoryNonceStoreis process-local. For multiple instances behind a load balancer, implement a sharedNonceStore, preferably with an atomicconsumemethod. The SDK README shows a Redis store (one key per nonce,SET ... NXwith an expiry that outlasts the challenge) and what a SQL one has to do. With onlyseenandadd, two instances that receive one response at the same moment can both admit it. - Pruning removes expired entries opportunistically; it does not replace the replay check.
classDiagram
class SigvaraGate {
-verifier : SigvaraVerifier
-nonces : NonceStore
-ttl : number
-inFlight : Set~string~
+threshold : number
+audience : string
+challenge(did) Challenge
+admit(did, challenge, signature) AdmitResult
-consumeNonce(nonce, expiresAt, nowSeconds) boolean
}
class NonceStore {
<<interface>>
+seen(nonce) boolean | Promise<boolean>
+add(nonce, expiresAt) void | Promise<void>
+prune(nowSeconds) void | Promise<void>
+consume?(nonce, expiresAt) boolean | Promise<boolean>
}
class MemoryNonceStore {
-spent : Map~string,number~
+seen(nonce) boolean
+add(nonce, expiresAt) void
+prune(nowSeconds) void
+consume(nonce, expiresAt) boolean
}
SigvaraGate --> NonceStore : "uses"
MemoryNonceStore ..|> NonceStore : "implements"
Diagram sources
Section sources
- gate.ts:14-26
- gate.ts:62-116
- gate.ts:190-214
- gate.test.ts:124-147
- gate.test.ts:173-195
- README.md:180-240
The gate distinguishes five refusal reasons so callers can respond appropriately. stale_score appears only when you set maxScoreAgeSeconds.
| Reason | Meaning | Recommended Caller Action |
|---|---|---|
bad_proof |
Signature verification failed, wrong agent, wrong audience, or malformed payload. | Reject the request; do not treat it as a reputation problem. |
replayed |
Valid signature but nonce already spent. | Ask for a fresh challenge; investigate possible replay attempts. |
not_active |
Agent proved itself but is not bonded, suspended, or slashed. "Bonded" is the bond held now against the minimum stake in force now, not the status stored when the agent was admitted (alpha.9 checks the stored status only). | Do not suggest earning points; address standing instead. |
stale_score |
Only with maxScoreAgeSeconds: the score was written longer ago than the limit, or never. |
Tell the operator the oracle has stopped writing, not that the score is low. |
below_threshold |
Agent is active and proved itself, but matured score is below threshold. | Inform the agent they need more reputation; include the current score. |
Only below_threshold is about reputation. Collapsing all failures into a single boolean makes debugging impossible.
Section sources
The gate compares the agent’s matured total score against the configured threshold. It uses the same value the reputation contract uses for threshold checks, avoiding disagreement with on-chain policies built on meetsThreshold. The gate also checks the bond held now against the minimum stake in force now, as SigvaraReader.isTrusted does, so the two agree for an Active agent whose bond is below the current minimum (alpha.9 checks the stored status and the score only, and admits such an agent).
Key points:
- The gate reads the matured total score, not the raw sum of factors.
- Raw factors may temporarily exceed the matured total while maturity ramps up.
- The maturity mechanism prevents farm-and-cash-out attacks by limiting how fast earned reputation becomes spendable.
- On networks where external trust and propagation are not yet reachable, the practical ceiling is lower than 100, so thresholds should be chosen carefully.
flowchart TD
Earned["Earned score finalized"] --> Matured["Matured score climbs toward earned"]
Matured --> Spendable["Spendable for gates and contracts"]
Earned --> RawFactors["Raw factor breakdown"]
RawFactors --> NotUsedByGate["Gate does not use raw sum"]
Matured --> ThresholdCheck["Gate compares matured score to threshold"]
Diagram sources
Section sources
To integrate reputation-based access control:
- Create a
SigvaraGatewith your RPC URL, contract addresses, chain ID, threshold, and a stable audience. The constructor throws if the audience is missing, the threshold is not a finite number of 0 or more, orttlSecondsis not a positive number. A threshold taken fromNumber(process.env.X)withXunset isNaN: on alpha.9 that admits every active agent that can prove its key, whatever its score, and from 1.0.0-alpha.10 the constructor throws. To refuse a score nobody has refreshed, also setmaxScoreAgeSeconds, a few times your oracle's epoch and measured against your host's clock (off by default, one more read per admit). - When a user wants to perform a protected action, call
challenge(did)and return the challenge object to the client. - Have the agent sign
challenge.payload. The agent should usesignChallengeFor(payload, audience), which signs only a challenge addressed to the audience it passes (alpha.9 has onlysignChallenge, which signs whatever it is handed; the agent'ssignChallengeis removed in 1.0.0-alpha.12). - Call
admit(did, challenge, signature)and inspect the result. - If
okis false, handle the specificreason.
For multi-instance deployments, provide a shared NonceStore with an atomic consume method.
Section sources
Use the gate as an authorization middleware:
- Protect routes or API endpoints by calling the gate before executing business logic.
- Map refusal reasons to HTTP status codes or application-specific errors.
- Cache or log decisions for observability, but do not cache admission results across challenge lifetimes.
- Treat
not_activedifferently frombelow_thresholdin user-facing messaging.
Section sources
-
bad_proof: reject immediately; ask the client to retry with a fresh challenge if needed. -
replayed: reject and warn about replay attempts; require a new challenge. -
not_active: inform the operator that the agent is not active; do not direct them to earn points. -
stale_score(only withmaxScoreAgeSeconds): tell the operator the oracle has stopped writing this score, or never wrote it; do not tell the agent to earn points. -
below_threshold: inform the agent that their reputation is too low; include the current score and threshold.
Section sources
- Challenges expire based on the signed timestamp plus the gate’s TTL.
- Nonces are dropped after expiry through opportunistic pruning.
- The default in-memory store keeps state bounded by issue rate rather than uptime.
- The default store is process-local.
- Behind a load balancer, implement a shared store with atomic
consume. - Without atomic consume, two concurrent requests with the same nonce could both observe it unspent; the gate adds an in-process guard, but that does not protect across processes.
- Invalid proofs never consume nonces, preventing denial-of-service via nonce exhaustion.
- Audience binding prevents relay attacks where one service’s response is reused at another.
- Using matured score prevents short-lived farms from passing gates before maturity catches up.
- Threshold selection should account for the reachable ceiling when external trust and propagation are limited.
- A caller who can form a well-sized signature for a registered agent makes the gate do one identity read, because the key the signature is checked against lives on chain. Nothing else is read until the proof and the nonce pass, so rate-limit callers where the gate is exposed.
- A challenge dated more than 60 seconds ahead of the gate's clock is refused as not fresh, so a proof over a far-future timestamp cannot stay valid, and its nonce cannot sit in the store until a far-off expiry. Challenges the gate issues itself are dated by its own clock.
Section sources
- gate.ts:14-26
- gate.ts:190-214
- gate.test.ts:137-147
- gate.test.ts:198-225
- gate.test.ts:347-375
- reputation-model.md:226-243
- reputation-model.md:331-359
Recommended pattern:
- Server-side gate instance with a stable audience.
- Client-side agent signs challenge payloads.
- Middleware enforces admission before route handlers.
- Clear error messages distinguish authentication failures from reputation failures.
- Shared nonce store for horizontally scaled deployments.
- Monitoring for replay attempts and repeated
bad_prooffailures.
flowchart TD
Request["HTTP Request"] --> Auth["Authentication Layer"]
Auth --> GateCall["SigvaraGate.admit()"]
GateCall --> Decision{"Admitted?"}
Decision --> |No| Error["Return error with reason"]
Decision --> |Yes| Handler["Business Logic Handler"]
Error --> Log["Log reason and metadata"]
Handler --> Response["Authorized Response"]
The gate depends on:
-
SigvaraVerifierfor signature verification, activity checks, and reputation queries. -
NonceStorefor replay prevention. - Configuration values for threshold, audience, TTL, and chain connectivity.
graph LR
Gate["SigvaraGate"] --> Verifier["SigvaraVerifier"]
Gate --> NonceStore["NonceStore"]
Verifier --> Chain["Reputation Contract"]
Gate --> Config["GateConfig"]
Diagram sources
Section sources
- The gate reads none of the status, the bond and the score until the signature and the nonce have passed. Checking the signature costs one identity read, so a caller who can form a well-sized signature for a registered agent still makes the gate do that one read.
- It reads the matured total score directly rather than fetching the full reputation breakdown, minimizing contract calls.
- It reads the status, the bond and the score together (and the score's write time if
maxScoreAgeSecondsis set), so an admit costs two round trips and not three, and each read gives up after 30 seconds by default. - Nonce pruning runs opportunistically and stops at the first live entry in the default store, keeping admission cost proportional to recent activity rather than total lifetime state.
- Use a shared nonce store with atomic
consumein production fleets to avoid race conditions and reduce round trips.
Section sources
-
Repeated
bad_proof: Check that the client signschallenge.payloadand returns the correct signature format. Ensure the audience matches the gate configuration. -
Unexpected
replayed: Ensure the client requests a fresh challenge for each attempt. If using multiple server instances, configure a shared nonce store with atomicconsume. -
Agents stuck at
not_active: Investigate bonding, suspension, or slashing status. Do not advise earning points as the fix. -
Agents failing at
below_threshold: Review the current score and threshold. Consider adjusting the threshold relative to the reachable reputation ceiling. -
The constructor throws: The audience is missing, the
thresholdis not a finite number of 0 or more, orttlSecondsormaxScoreAgeSecondsis not a positive number. On alpha.9 only the audience is checked. -
admitwaits a long time: The RPC has gone quiet. A read is abandoned after 30 seconds, andtimeoutMschanges that. On alpha.9 ethers waits five minutes.
- Inspect the
reasonfield inAdmitResult; it tells you exactly which layer failed. - Confirm that the nonce comes from the signed payload, not a caller-modified wrapper.
- Verify that the TTL and audience are set consistently across clients and servers.
- Monitor nonce store growth and pruning behavior in production.
Section sources
The Sigvara SDK gate provides a focused, secure, and debuggable authorization layer. It combines challenge-response authentication, nonce-based replay protection, activity checks, and matured reputation threshold enforcement. By returning explicit refusal reasons and separating authentication from authorization, it enables integrators to build robust reputation-based access control for web applications and other systems.
When deploying in production, prioritize:
- A stable audience per service.
- A shared nonce store with atomic consume.
- Careful threshold selection aligned with the reachable reputation ceiling.
- Clear handling of each refusal reason in user-facing flows.
| Field | Description | Notes |
|---|---|---|
rpcUrl |
Ethereum RPC endpoint | Used by the verifier to read on-chain data |
addresses |
Contract addresses | Identity, reputation, and staking addresses |
chainId |
Optional chain identifier | Passed to the verifier, which uses it only when it builds a DID Document; the gate's own checks do not depend on it, and the DID in a challenge already carries its chain |
threshold |
Minimum matured score required | Choose against the reachable ceiling. Must be a finite number, 0 or more |
audience |
Stable service identifier | Signed into every challenge to prevent relays |
ttlSeconds |
Challenge validity duration | Default 300 seconds if omitted. Must be a positive number |
nonceStore |
Optional custom nonce store | Required for multi-instance safety |
timeoutMs |
How long one chain read may take, in milliseconds | Default 30000 (alpha.9 uses ethers' five minutes) |
maxScoreAgeSeconds |
Refuse a score written longer ago than this many seconds, or never, with stale_score
|
Optional and off by default. A positive number; choose a few times your oracle's epoch. Measured against the gate host's clock. Costs one more read per admit |
Section sources
- Agent Registration and Lifecycle
- Contract Overview and Architecture
- Economic Security Model and Incentives
- Evidence Verification and Merkle Proofs
- Key Management and Rotation
- Maturity Mechanism and Score Release
- Proposal Workflow and Challenge System
- Score Storage and Data Model
- SigvaraIdentity Contract
- SigvaraOracleBond Contract
- SigvaraReputation Contract
- SigvaraStaking Contract
- Slashing Lifecycle and Dispute Resolution
- Stake Management Functions
- Status Management and Control
- Token and Fee Contracts
- Unbonding Period and Withdrawal Security
- Anti-Manipulation and Security Measures
- Community Flag Processing
- Evidence Sources and Verification
- External Trust Integration
- Merkle Proof System
- Oracle Score Computation
- Payment-Backed Attestations
- Scoring Model and Formulas
- Agent Creation and Key Management
- Agent Operations
- Agent Registration Workflow
- Challenge-Response Authentication
- Gate Access Control
- Integration Patterns and Examples
- SDK Overview and Setup
- Verifier Functions