Skip to content

Security Model ​

How s402 handles trust, validation, and failure modes.

Trust Boundaries ​

Untrusted                    Trust Boundary                   Trusted
────────────────────────────────┬──────────────────────────────────────
  HTTP headers (base64 JSON)    │  Decoded, validated types
  Network responses             │  Scheme implementations
  User-provided config          │  Internal function calls

All data from HTTP headers is untrusted until it passes through a decode function (decodePaymentRequired, decodePaymentPayload, decodeSettleResponse). These functions validate the shape of the JSON and throw s402Error('INVALID_PAYLOAD') if anything is wrong.

After validation, internal code can rely on TypeScript types.

What s402 Validates ​

Decode-time (shape validation) ​

Every decode function checks required fields exist and have the correct types:

FunctionValidates
decodePaymentRequiredaccepts (array), network, asset, amount, payTo (all strings)
decodePaymentPayloadscheme (valid enum), payload (object)
decodeSettleResponsesuccess (boolean)

Scheme-specific (semantic validation) ​

Each scheme's verify() method validates business logic:

  • Exact: signature recovery, dry-run simulation, balance check
  • Stream: stream creation PTB structure, deposit amount
  • Escrow: deadline, arbiter address, escrow amount
  • Unlock: escrow creation, SEAL encryption ID match
  • Prepaid: deposit amount, rate per call, max calls cap

Facilitator process() (temporal validation) ​

Before dispatching to scheme verification:

  • Checks expiresAt against current time
  • Rejects expired requirements with REQUIREMENTS_EXPIRED

What s402 Does NOT Validate ​

The core s402 package is a protocol layer — it handles encoding, decoding, and dispatch. It does not:

  • Verify cryptographic signatures (that's the scheme implementation's job)
  • Check on-chain balances (requires Sui SDK / RPC, lives in your scheme implementation)
  • Broadcast transactions (scheme implementation)
  • Rate-limit requests (server middleware concern)
  • Authenticate callers (application concern)

This separation is intentional. The protocol layer stays small and auditable. Security-critical operations (signing, verification, settlement) live in scheme implementations where they can use the Sui SDK directly.

Supply Chain ​

  • Zero runtime dependencies — no node_modules attack surface in production
  • Dev dependencies only — TypeScript, tsdown, vitest (build/test tools)
  • ESM-only — no dual CJS/ESM complexity that can lead to module confusion attacks

Error Recovery ​

Every s402Error includes:

FieldPurpose
codeMachine-readable error type
retryableWhether the client should try again
suggestedActionHuman-readable recovery hint

This prevents clients from guessing whether to retry. If retryable is false, the client knows to take a different action (top up balance, get a new mandate, switch schemes).

Scheme-Specific Security ​

Exact ​

  • One transaction per request. No state between calls.
  • The facilitator must call waitForTransaction() before returning success. Without finality confirmation, the server could grant access for a transaction that gets reverted.

Upto ​

  • Variable-amount settlement with on-chain cap enforcement.
  • settlementCeiling (client-chosen) is enforced by the Move contract — the facilitator cannot settle above this amount.
  • Deadline-based expiry: if the facilitator doesn't settle before settlementDeadlineMs, the payer reclaims the full deposit via expire().
  • The server reports actualAmount at settle-time. The server is trusted to report honest usage; the facilitator enforces actualAmount ≤ min(maxAmount, settlementCeiling).

Prepaid ​

Provider-side enforcement (trustless):

  • On-chain rate caps: claimed ≤ maxCalls × ratePerCall — hard ceiling enforced by Move contract
  • On-chain balance cap: claimed ≤ deposited amount — provider cannot claim beyond deposit
  • Withdrawal delay prevents immediate drain after deposit; gives provider time to claim what they legitimately earned before the agent reclaims funds

Agent-side mitigations (economic):

  • Small deposits + short refill cycles: Limit exposure to one deposit cycle. The worst-case overclaim is bounded by deposit size, not your total budget.
  • Evaluate withdrawalDelayMs before depositing: Set by the provider. High values lock your capital. Check it before committing funds to an unfamiliar provider.
  • Prefer providers advertising providerPubkey: Signals v0.2 support — signed receipts per call, enabling cryptographic fraud proofs if overclaiming occurs.

Trust model: The contract enforces a maximum, not an exact count. A provider can claim up to the cap regardless of actual calls served. This is a trust-bounded model — appropriate for commercial relationships, not adversarial zero-trust scenarios. For the latter, use Escrow.

v0.2 signed receipts (opt-in): When providers include providerPubkey (Ed25519) in requirements, they sign a receipt for every call served. Agents accumulate these and can produce a fraud proof if claimed count exceeds signed receipt count. Backward-compatible — providers without providerPubkey use v0.1 behavior.

Horizontal scaling: Multi-instance providers must coordinate off-chain usage counters to prevent double-authorization across instances.

Escrow ​

  • Time-locked vault: funds locked on-chain, not transferred until resolution
  • Deadline enforcement: permissionless refund after deadline — anyone can trigger, no arbiter required
  • Optional arbiter: An arbiter is a trusted third party for dispute resolution. Without one, the only resolution paths are buyer-confirms or deadline-expires. Arbiters are optional by design; for high-value transactions, choose an arbiter you trust independently of the protocol.

Stream ​

  • Per-second rate cap enforced on-chain
  • Budget cap prevents runaway spending
  • Minimum deposit ensures the stream is worth creating

Unlock ​

  • Two-stage flow: TX1 creates escrow receipt, TX2 passes receipt to seal_approve
  • Sui's seal_approve requires all arguments as Argument::Input — this constraint forces the two-stage design
  • Encrypted content on Walrus is only decryptable after payment

Direct Settlement ​

When using s402DirectScheme, the client builds, signs, and broadcasts the transaction itself. The settleDirectly() interface must call waitForTransaction() before returning success.

Without the finality wait, a client could report success for a transaction that hasn't been confirmed, creating a race condition with the resource server.

Reporting Vulnerabilities ​

If you find a security issue, please report it privately via either method:

We take security seriously and will respond within 48 hours. See SECURITY.md for full disclosure policy and scope.

Released under the Apache 2.0 License.