Skip to main content

FPSF-SS-002 — wallet-gateway Interface

Document Metadata

FieldValue
Spec IDFPSF-SS-002
TitleStablecoin Stack — wallet-gateway Interface
Version1.0.0
StatusDraft
Date2026-03-25
AuthorAdalton Reis — reis@fabricpaymentstandards.org
OrganizationFabric Payment Standards Foundation
Contactspecs@fabricpaymentstandards.org>
LicenseApache-2.0
Conforms ToFPSF-SS-001 v1.0.0

Table of Contents

  1. Abstract
  2. Introduction
  3. Role Within the Stablecoin Stack
  4. Transport and Connection Model
  5. Authentication and Message Security
  6. Token and Domain Separator Conventions
  7. Wallet Initialisation
  8. Request–Response Operations
  9. Subscription Operations
  10. Asynchronous Status Notifications
  11. Message Type Reference
  12. Error Handling
  13. Security Considerations
  14. Conformance
  15. Future Work

1. Abstract

This document specifies the interface of the wallet-gateway component of the Stablecoin Stack (FPSF-SS-001). The wallet-gateway is the sole network entry point for wallet client applications. It provides a WebSocket-based messaging interface through which wallets perform balance queries, history retrieval, nonce and fee lookups, payment submissions, and acquirer registration requests. It also delivers real-time notifications on submission status, balance changes, and new confirmed transfers.

All communication is authenticated by cryptographic signature on every message. No session tokens, cookies, or API keys are used. The gateway does not execute operations directly; it mediates between the wallet client and the internal broadcast and indexing services of the Stablecoin Stack.


2. Introduction

2.1 Purpose

This specification defines the normative interface that a conformant wallet-gateway MUST implement, and that a conformant wallet client MUST be capable of consuming.

2.2 Scope

This specification covers: the transport model and connection management requirements; the authentication scheme; all request-response operations; the subscription model; the asynchronous status notification model; error categories and response formats; and security requirements specific to the gateway interface.

This specification does not cover: the internal protocol between wallet-gateway and broadcast-service; the internal protocol between wallet-gateway and balance-and-history; the on-chain Settlement Contract interface (FPSF-SS-001 Sections 11–14); or wallet key management and signing UX.

2.3 Normative References

ReferenceDescription
FPSF-SS-001 v1.0.0Stablecoin Stack — foundational specification
ERC-20Token Standard
ERC-2612Permit Extension for EIP-20 Signed Approvals
EIP-712Typed Structured Data Hashing and Signing
ECDSA / FIPS 186-4Digital Signature Standard
RFC 2119Key words for use in RFCs to Indicate Requirement Levels
RFC 6455The WebSocket Protocol
RFC 8446The Transport Layer Security (TLS) Protocol Version 1.3

2.4 Conventions and Terminology

The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119.

All terms defined in FPSF-SS-001 Section 2.4 carry the same meaning here. Additional terms:

TermDefinition
Wallet ClientA conformant client application connecting to the wallet-gateway on behalf of a payer.
Gateway MessageA JSON object transmitted over the WebSocket connection, conforming to the message envelope in Section 5.2.
Caller AddressThe EVM address identifying the wallet in a given message. MUST match the address recovered from the message signature.
Domain SeparatorA bytes32 EIP-712 domain separator hex string. Used as the primary token identifier throughout the gateway interface.
SubscriptionA per-connection registration to receive real-time push notifications for specified tokens. Active only while the originating connection is open.
Idle TimeoutThe gateway-configurable duration after which a connection with no inbound activity is closed. Recommended: 5 minutes.
BrokenDownAmountA response object carrying operatorFee, acquiringFee, and totalWithFees fields.
Submission StatusOne of the ordered states a submission passes through: ENQUEUING, PENDING, BROADCASTING, SUCCESS, FAILURE.
Terminal StateA submission status from which no further transitions occur: SUCCESS or FAILURE.

3. Role Within the Stablecoin Stack

The wallet-gateway occupies the boundary between external wallet clients and the internal services of the Stablecoin Stack. It holds no token balances and cannot initiate on-chain transactions. Its responsibilities are: accepting and authenticating WebSocket connections; routing validated requests to the appropriate internal service; delivering responses and push notifications; enforcing the single-connection-per-wallet constraint; and closing idle connections with resource cleanup.

The gateway MUST NOT execute payment operations directly. Upon receiving a valid submission, it MUST enqueue the request with the broadcast-service and relay status updates to the client.


4. Transport and Connection Model

4.1 WebSocket-Only Transport

The wallet-gateway MUST expose its interface exclusively over the WebSocket protocol (RFC 6455). All WebSocket connections MUST be established over TLS (WSS). Plaintext WebSocket connections MUST be refused.

4.2 Single-Connection-Per-Wallet Constraint

A maximum of one active connection per wallet address is permitted at any time. When a new connection is established and authentication succeeds for an address that already has an active connection, the gateway MUST:

  1. Accept the new connection.
  2. Terminate the existing connection with close code 4001 and reason "superseded".
  3. Discard any pending notifications queued for the previous connection.

4.3 Connection Lifecycle and Idle Timeout

The gateway MUST enforce an idle timeout (RECOMMENDED: 5 minutes). The idle timer resets on every valid inbound message. Before closing an idle connection, the gateway SHOULD send a WebSocket ping frame. If the client responds with pong, the timer MUST reset.

Upon closing a connection for any reason, the gateway MUST immediately release all associated resources: all active subscriptions; any in-memory notification state; and the wallet address slot.

Subscription state is never persisted across connections. A reconnecting client MUST re-register all required subscriptions.

Upon establishing a new connection, the client MUST send an authenticated message within a gateway-configurable timeout (RECOMMENDED: 30 seconds). Connections that do not produce an authenticated message within this window MUST be closed.


5. Authentication and Message Security

5.1 Signature-Based Authentication

Every message sent by a wallet client MUST be individually signed by the private key corresponding to the wallet's address. The gateway MUST verify this signature on every received message, without exception.

5.2 Message Envelope

All messages sent by a wallet client MUST conform to:

interface GatewayMessage {
type: string; // Message type identifier. See Section 11.
callerAddress: string; // EVM address of the sender. 42-char hex.
deadline: number; // Unix timestamp (seconds). Reject if expired.
payload: object; // Operation-specific parameters.
signature: MessageSig;
}

interface MessageSig {
hash: string; // bytes32 — 66-char hex. EIP-712 digest of the message.
v: number; // uint8. Must be 27 or 28 (0/1 normalized automatically).
r: string; // bytes32 — 66-char hex.
s: string; // bytes32 — 66-char hex.
}

5.3 Signature Verification

The gateway MUST perform the following checks in order, rejecting immediately on any failure:

  1. Structure: all envelope fields present and correctly typed.
  2. Deadline: deadline strictly greater than gateway clock. Apply configurable tolerance (RECOMMENDED: 30 seconds) for clock skew.
  3. Signature format: v in {0, 1, 27, 28}. Normalize 0→27, 1→28. Reject all other values. r and s MUST be valid 32-byte hex strings.
  4. Hash verification: gateway recomputes the EIP-712 digest and verifies it equals signature.hash.
  5. Signature recovery: recover the signing address from (hash, v, r, s).
  6. Address match: recovered address MUST equal callerAddress.

The gateway MUST NOT partially process a message that fails any step.

5.4 Expiration and Replay Protection

The deadline field provides time-bounded validity. The gateway SHOULD maintain a short-lived cache of processed message hashes to detect and reject duplicates within the validity window. Primary replay protection for payment operations is enforced by usedHashes in the Settlement Contract (FPSF-SS-001 Section 19.3).


6. Token and Domain Separator Conventions

Throughout the gateway interface, supported tokens are identified primarily by their EIP-712 domain separator — a bytes32 hex string uniquely identifying a token contract within a specific network and version context. Full token metadata is available from the Basic Data Service.

The two exceptions where a token is identified by its contract address rather than its domain separator are the PayWithPermitParams and BuyAcquiringPackPermitParams structures (defined in FPSF-SS-001 Sections 8.3 and 8.4), which require the token address for on-chain permit validation.

The gateway MUST reject any request referencing a domain separator or token address not corresponding to a supported token.


7. Wallet Initialisation

When a wallet address connects for the first time — no prior state exists — the gateway MUST transparently perform:

  1. History collection: retrieve the complete transfer history for the address across all supported tokens.
  2. Balance snapshot: record current balances across all supported tokens.

This initialisation is transparent to the client — no explicit request is required. If initialisation is still in progress when a balance or history query arrives, the gateway MUST respond with an INITIALISING error (Section 12) rather than returning incomplete data.

Subsequent connections from the same address do not trigger re-initialisation.


8. Request–Response Operations

All operations use the message envelope in Section 5.2. The gateway MUST respond on the same WebSocket connection with the corresponding response type. All response messages carry a requestId field echoing the originating request's requestId.

8.1 Retrieve Nonce

Retrieves the current ERC-2612 permit nonce for a wallet address and token.

Request type: GET_NONCE

interface GetNoncePayload {
requestId: string;
domainSeparator: string; // bytes32 hex — domain separator of target token.
}

Response type: NONCE_RESULT

interface NonceResultPayload {
requestId: string;
domainSeparator: string;
nonce: string; // Current permit nonce as a decimal string (uint256).
}

The gateway MUST retrieve the nonce from live on-chain state. Stale nonces MUST NOT be returned.

8.2 Retrieve Fees

Returns the fee breakdown for a given transfer amount, token, and acquirer. Reflects the per-token fee model defined in FPSF-SS-001 Section 13.

Request type: GET_FEES

interface GetFeesPayload {
requestId: string;
domainSeparator: string; // Token for which fees are being calculated.
principal: string; // Principal amount intended for the beneficiary (decimal string).
acquirerId: string; // bytes16 hex (34-char). Pass Zero-UUID if no acquirer.
}

Response type: FEES_RESULT

interface FeesResultPayload {
requestId: string;
domainSeparator: string;
brokenDownAmount: BrokenDownAmount;
}

interface BrokenDownAmount {
operatorFee: string; // Total operator fee in token units (decimal string).
acquiringFee: string; // Acquiring fee in token units. "0" if no acquirer.
totalWithFees: string; // principal + operatorFee + acquiringFee. This is PermitParams.value.
}

The values correspond directly to calculateFees(token, principal, acquirerId) on the Settlement Contract (FPSF-SS-001 Section 13.4).

8.3 Retrieve Balance

Returns the current token balances for the wallet across one or more tokens.

Request type: GET_BALANCE

interface GetBalancePayload {
requestId: string;
domainSeparators: string[]; // One or more domain separator hex strings.
}

Response type: BALANCE_RESULT

interface BalanceResultPayload {
requestId: string;
balances: TokenBalance[];
}

interface TokenBalance {
domainSeparator: string;
balance: string; // Current balance in token units (decimal string).
}

If a domain separator is not recognized, the gateway MUST return an error for the entire request.

8.4 Retrieve Transfer History

Returns the transfer history for the wallet for one or more supported tokens. Includes only ERC-20 Transfer events for supported tokens.

Request type: GET_HISTORY

interface GetHistoryPayload {
requestId: string;
domainSeparators: string[];
cursor?: string; // Optional pagination cursor.
limit?: number; // Gateway MAY enforce a ceiling.
}

Response type: HISTORY_RESULT

interface HistoryResultPayload {
requestId: string;
transfers: TransferRecord[];
nextCursor?: string;
}

interface TransferRecord {
domainSeparator: string;
txHash: string; // 66-char hex.
blockNumber: number;
timestamp: number; // Unix timestamp of the block.
from: string; // EVM address.
to: string; // EVM address.
value: string; // Amount in token units (decimal string).
direction: "IN" | "OUT";
}

Results MUST be returned in reverse chronological order (most recent first).

8.5 Submit Payment Request

Submits a signed TransferRequest (FPSF-SS-001 Section 9.2) for processing. This is an asynchronous operation; the gateway acknowledges receipt immediately and delivers status updates via Section 10.

Request type: SUBMIT_PAYMENT

interface SubmitPaymentPayload {
requestId: string;
transferRequest: TransferRequest; // As defined in FPSF-SS-001 Section 9.2.
// Note: payWithPermitParams contains separate
// orderReference and acquirerId bytes16 fields.
}

Response type: SUBMIT_PAYMENT_ACK

interface SubmitPaymentAckPayload {
requestId: string;
payloadId: string; // Echoes transferRequest.payloadId.
status: "ENQUEUING";
}

If the submission fails envelope or first-line validation, the gateway MUST return an ERROR response rather than an acknowledgment.

8.6 Submit Acquirer Registration Request

Submits a signed BuyAcquiringPackRequest (FPSF-SS-001 Section 9.3) for processing. Asynchronous in the same manner as payment submission.

Request type: SUBMIT_ACQUIRING

interface SubmitAcquiringPayload {
requestId: string;
buyAcquiringPackRequest: BuyAcquiringPackRequest; // As defined in FPSF-SS-001 Section 9.3.
// Note: acquiringFeeBps_ is a dedicated field.
}

Response type: SUBMIT_ACQUIRING_ACK

interface SubmitAcquiringAckPayload {
requestId: string;
status: "ENQUEUING";
}

9. Subscription Operations

Subscriptions allow a connected wallet to receive real-time push notifications without polling. All subscriptions are scoped to the originating connection and cancelled when the connection closes for any reason.

9.1 Subscribe to Balance Updates

Request type: SUBSCRIBE_BALANCE

interface SubscribeBalancePayload {
requestId: string;
domainSeparators: string[];
}

Response type: SUBSCRIBE_BALANCE_ACK

interface SubscribeBalanceAckPayload {
requestId: string;
subscribedSeparators: string[];
}

Push notification type: BALANCE_UPDATE

interface BalanceUpdatePayload {
domainSeparator: string;
balance: string; // New balance in token units (decimal string).
}

9.2 Subscribe to Transfer Notifications

Request type: SUBSCRIBE_TRANSFERS

interface SubscribeTransfersPayload {
requestId: string;
domainSeparators: string[];
}

Response type: SUBSCRIBE_TRANSFERS_ACK

interface SubscribeTransfersAckPayload {
requestId: string;
subscribedSeparators: string[];
}

Push notification type: TRANSFER_NOTIFICATION

interface TransferNotificationPayload {
transfer: TransferRecord; // As defined in Section 8.4.
}

9.3 Unsubscribe

Request type: UNSUBSCRIBE

interface UnsubscribePayload {
requestId: string;
channel: "BALANCE" | "TRANSFERS";
domainSeparators: string[];
}

Response type: UNSUBSCRIBE_ACK

interface UnsubscribeAckPayload {
requestId: string;
channel: "BALANCE" | "TRANSFERS";
unsubscribedSeparators: string[];
}

10. Asynchronous Status Notifications

10.1 Submission Status Lifecycle

StatusTerminalDescription
ENQUEUINGNoAccepted by gateway; being handed to broadcast-service.
PENDINGNoReceived by broadcast-service; full validation in progress.
BROADCASTINGNoValidation passed; transaction being submitted to the network.
SUCCESSYesNetwork accepted the transaction without revert. NOT final settlement.
FAILUREYesSubmission failed. See failureCategory and failureReason.

SUCCESS is not final settlement. Final settlement is confirmed only when transfer-history observes the PermittedTransfer event with sufficient block confirmations, delivered as a TRANSFER_NOTIFICATION.

10.2 Status Notification Message

Push notification type: SUBMISSION_STATUS

interface SubmissionStatusPayload {
payloadId: string;
submissionType: "PAYMENT" | "ACQUIRING";
status: SubmissionStatus;
failureReason?: string;
failureCategory?: FailureCategory;
txHash?: string; // Present when status is BROADCASTING or SUCCESS.
}

type SubmissionStatus = "ENQUEUING" | "PENDING" | "BROADCASTING" | "SUCCESS" | "FAILURE";
type FailureCategory = "STRUCTURAL_ERROR" | "SEMANTIC_ERROR" | "CRYPTOGRAPHIC_ERROR" | "BROADCAST_ERROR";

Failure categories correspond to those defined in FPSF-SS-001 Section 18.


11. Message Type Reference

TypeDirectionDescription
GET_NONCEClient → GatewayRequest current permit nonce.
NONCE_RESULTGateway → ClientCurrent nonce.
GET_FEESClient → GatewayRequest fee breakdown (per-token, with separate acquirerId).
FEES_RESULTGateway → ClientBrokenDownAmount with operatorFee, acquiringFee, totalWithFees.
GET_BALANCEClient → GatewayRequest balances for one or more tokens.
BALANCE_RESULTGateway → ClientBalance array.
GET_HISTORYClient → GatewayRequest transfer history.
HISTORY_RESULTGateway → ClientTransfer records.
SUBMIT_PAYMENTClient → GatewaySubmit a TransferRequest payload.
SUBMIT_PAYMENT_ACKGateway → ClientAcknowledgment with initial ENQUEUING status.
SUBMIT_ACQUIRINGClient → GatewaySubmit a BuyAcquiringPackRequest payload.
SUBMIT_ACQUIRING_ACKGateway → ClientAcknowledgment with initial ENQUEUING status.
SUBSCRIBE_BALANCEClient → GatewaySubscribe to balance update notifications.
SUBSCRIBE_BALANCE_ACKGateway → ClientSubscription confirmation.
SUBSCRIBE_TRANSFERSClient → GatewaySubscribe to transfer notifications.
SUBSCRIBE_TRANSFERS_ACKGateway → ClientSubscription confirmation.
UNSUBSCRIBEClient → GatewayCancel a subscription.
UNSUBSCRIBE_ACKGateway → ClientCancellation confirmation.
BALANCE_UPDATEGateway → ClientPush: balance changed on a subscribed token.
TRANSFER_NOTIFICATIONGateway → ClientPush: new confirmed transfer on a subscribed token.
SUBMISSION_STATUSGateway → ClientPush: submission status transition.
ERRORGateway → ClientError response for any failed request.

12. Error Handling

Error message type: ERROR

interface ErrorPayload {
requestId?: string;
errorCode: string;
errorCategory: ErrorCategory;
message: string; // Human-readable. MUST NOT include internal implementation detail.
}

type ErrorCategory =
| "STRUCTURAL_ERROR"
| "AUTHENTICATION_ERROR"
| "SEMANTIC_ERROR"
| "NOT_FOUND"
| "RATE_LIMIT"
| "INTERNAL_ERROR";
Error CodeCategoryDescription
MISSING_FIELDSTRUCTURAL_ERRORA required envelope or payload field is absent.
INVALID_FORMATSTRUCTURAL_ERRORA field does not conform to its expected format.
INVALID_SIGNATUREAUTHENTICATION_ERRORSignature malformed or cannot be verified.
ADDRESS_MISMATCHAUTHENTICATION_ERRORRecovered signer does not match callerAddress.
EXPIRED_DEADLINEAUTHENTICATION_ERRORMessage deadline is in the past.
UNSUPPORTED_TOKENSEMANTIC_ERRORDomain separator or token address not recognized.
NONCE_MISMATCHSEMANTIC_ERRORSupplied nonce does not match on-chain state.
UNKNOWN_PAYLOAD_IDSEMANTIC_ERRORpayloadId does not correspond to a known session.
ALREADY_SUBMITTEDSEMANTIC_ERRORpayloadId has already been processed.
WALLET_NOT_FOUNDNOT_FOUNDNo state found for the given wallet address.
INITIALISINGSEMANTIC_ERRORWallet state still being collected. Retry shortly.
RATE_LIMIT_EXCEEDEDRATE_LIMITRequest rate exceeded.
INTERNAL_ERRORINTERNAL_ERRORUnexpected gateway-side failure.

The gateway MUST NOT include stack traces, internal service identifiers, or implementation-sensitive detail in error responses.


13. Security Considerations

Per-message signature verification. The gateway MUST verify the message signature on every received message without exception.

Single-connection enforcement. Prevents a stale or compromised session from continuing to receive notifications after a new authorized connection supersedes it.

Idle timeout and resource cleanup. Bounds the period during which server-side resources, including subscription registrations, are held for a non-communicating client.

No key material exposure. Wallet clients MUST NOT transmit private keys, seed phrases, or any key derivation material to the gateway. All signing MUST be performed locally.

TLS requirement. All connections MUST use TLS. The gateway MUST refuse plaintext connections.

Rate limiting. The gateway SHOULD implement per-connection and per-address rate limiting. Rate-limit errors MUST use the RATE_LIMIT error category.

No fund custody. The gateway holds no token balances. A gateway compromise does not directly result in loss of funds — cryptographic integrity is enforced by the Settlement Contract independently.


14. Conformance

14.1 Conformant wallet-gateway

A conformant wallet-gateway MUST:

  • Expose its interface exclusively over WebSocket with TLS (Section 4.1).
  • Enforce the single-connection-per-wallet constraint with most-recent-connection precedence (Section 4.2).
  • Enforce an idle timeout and release all connection resources on close (Section 4.3).
  • Verify the message signature on every received message, rejecting on any failure in the sequence of Section 5.3.
  • Perform transparent wallet initialisation on first connection and respond with INITIALISING if a query arrives before completion (Section 7).
  • Implement all request-response operations defined in Section 8, including the updated GET_FEES response structure with separate operatorFee, acquiringFee, and totalWithFees fields.
  • Accept SUBMIT_PAYMENT payloads containing PayWithPermitParams with separate orderReference and acquirerId bytes16 fields, and forward them correctly to the broadcast-service.
  • Accept SUBMIT_ACQUIRING payloads containing BuyAcquiringPackPermitParams with the acquiringFeeBps_ field.
  • Implement the subscription model of Section 9 with subscriptions scoped to the originating connection.
  • Deliver SUBMISSION_STATUS notifications through to a terminal state for all accepted submissions (Section 10).
  • Return structured ERROR messages conforming to Section 12 for all failures.

14.2 Conformant wallet client

A conformant wallet client MUST:

  • Connect exclusively via WSS.
  • Include a valid signature in every message sent to the gateway.
  • Handle unexpected disconnection gracefully and re-establish the connection when required.
  • Re-register all required subscriptions after reconnection.
  • Construct PayWithPermitParams with orderReference and acquirerId as separate bytes16 fields — not concatenated.
  • Call GET_FEES with the correct acquirerId (or Zero-UUID) and use the totalWithFees value as PermitParams.value.
  • Not treat a SUCCESS submission status as final settlement.
  • Not transmit key material of any kind to the gateway.

15. Future Work

Per-token fee model display. The GET_FEES response already returns operatorFee and acquiringFee separately. A future MINOR version may add a feeBreakdown field with explicit baseFee and basisPointsFee sub-components for UI display purposes.

Person-to-person transfers. The architecture supports P2P token transfers. A future wallet specification (FPSF-SS-005) will define the client-side interface for this use case.

Wallet certification. FPSF-SS-005 (planned) will define a conformance test suite and certification programme.


FPSF-SS-002 v1.0.0 · Draft · Fabric Payment Standards Foundation · Apache-2.0