Durable plans and signed transaction relay
A plan stores the exact proposed transactions for review. Relay means submitting transactions a wallet has signed. Center stores the reviewed calls unchanged and tracks their results. It never holds wallet keys, signs transactions, assumes token spending approval, or gives spending permission through an API credential.
The service supports wallets controlled directly by a signing key (EOAs), calls forwarded by prepaid, and separately configured smart-wallet requests (UserOperations) through EntryPoint v0.7. Direct relay accepts one exact wallet-signed EVM transaction matching one planned call. See the glossary.
The direct transport supports EIP-155 protected legacy, EIP-2930, and EIP-1559 transactions. Contract creation without a destination, unprotected legacy transactions, blobs, and EIP-7702 authorization-list transactions are rejected. Factory calls with an explicit reviewed destination remain ordinary supported calls. Runtime capabilities report each transport's configured availability, confirmations, and fee bounds. The separate prepaid sponsorship service reports relayr-prepaid-erc2771 funding requirements; userOperations and sessions report hosted EntryPoint execution and recurring authority. A route or account binding alone does not establish availability.
Workflow
The encoded call instructions are calldata. Canonical block evidence comes from a block that remains in the chain's accepted history. An idempotency key identifies retries of the same operation; keep it unchanged.
- A protocol operation or reviewed ABI method produces
RestPlanDraft: wallet account, exact chain/destination/calldata/native value, ordered dependencies, and canonical block evidence. The authenticated account must own the planned wallet. A bot principal is an API identity, not the wallet signer. createPlan(actor, draft, idempotencyKey, requestHash)returns a durable plan ID, commitment, expiry, calls, and redacted step state. The body digest is computed by the authenticated HTTP boundary. An identical idempotency retry returns the original plan, including after its expiry; another body or operation conflicts.simulateStep(actor, id, stepIndex)checks dependencies and canonical preparation evidence, then simulates and estimates the exact call as the wallet at a reported canonical block. It never supplies state overrides. Approval prerequisites must already be confirmed and semantically verified when a verifier models them.- The wallet signs the exact transaction.
submitStep(actor, id, stepIndex, rawSignedTransaction, idempotencyKey, requestHash)recovers the signer and validates chain, destination, calldata, value, signature form, nonce, access list bounds, gas, fees, and maximum cost. A new relay requires the signed nonce to equal the account's current pending nonce; future nonce queues and replacements are excluded. - The service reconciles the deterministic transaction hash first. If the hash is already observed, it records monitoring state without broadcasting. Otherwise it simulates and estimates against current canonical state, atomically claims a durable dispatch lease, and submits the exact supplied bytes.
refresh(actor, id)checks receipt status, canonical block hash, configured confirmations, and any installed semantic verifier. Unavailable evidence remains unknown. A successful source-chain receipt does not establish cross-chain settlement.
An EOA transaction signature has no trustworthy signing timestamp. Plan expiry constrains admission to this relay; it is not an onchain deadline and does not prove when a signature was produced. Contract-level deadlines and a separate owner approval bound to an execution commitment are distinct controls. Old signed transactions may remain broadcastable outside this service while their nonce remains unused.
Production installs authorizeDispatch(plan, stepIndex, transactionHash) in TransactionService. After canonical preflight, it verifies a fresh owner-signed API request or the separate owner approval bound to the exact account, principal, plan commitment, step, and raw transaction hash. A bot API credential and an otherwise valid raw EOA transaction signature do not establish this freshness. The callback returns verified issuedAt and expiresAt Unix seconds. The service checks the window immediately and the store rechecks it using the admission clock after lock waits: validity is at most five minutes, future issuance tolerance is 30 seconds, and expiry must remain in the future. Read-only observed-hash reconciliation and active-lease replays acquire no new dispatch and need no new owner approval. Capabilities distinguish this configured production mode from a trusted internal service constructed without the callback.
maximumCost and the configured maximumTransactionCost admission bound cover native value plus gas limit times the signed fee cap. They do not cap or quote additional chain-specific fees. For example, OP Stack chains add L1 data fees and may add operator fees to execution gas charges; see the OP Stack fee documentation. The public costScope field makes this limitation explicit. Wallets must account for their chain's full fee model; the relay still runs exact-call gas estimation and canonical-state preflight.
Persistence, revocation, and retries
MemoryTransactionStore is intended for tests or explicitly ephemeral local use. PostgresTransactionStore uses migration 005_rest_transactions.sql, durable plans, actor-scoped idempotency records, and permanent sender/chain/nonce reservation tombstones. Migration 006_rest_sponsorship.sql supplies the shared plan-step transport reservation table; run all migrations before admitting transaction requests. Public service responses omit signed bytes and lease tokens. The storage layer retains signed bytes solely for exact-hash identity and recovery; operators must protect the database and backups accordingly.
The account authorization check and dispatch claim share the account row lock used by grant revocation. The claim is the admission boundary: revocation prevents new or recovered dispatch claims, while an already claimed, bounded in-flight request may finish. No database transaction stays open during RPC. Expired leases must be reclaimed under current authorization; read-only receipt reconciliation does not reacquire signing or relay authority.
Every step binds permanently to one raw transaction/hash. Every sender/chain/nonce binds globally to one plan step, including across grants. Reusing an idempotency key with a different request conflicts. A lost response, timeout, process crash, or lease expiration does not authorize different calldata, another nonce, or a fee replacement. Reconcile the stored hash. A user may explicitly retry the identical bytes only after a new claim, fresh canonical preflight, and an unchanged pending nonce; an active lease suppresses concurrent dispatch.
Each admitted plan step permanently selects exactly one transport and execution binding under the same account lock. A direct claim binds signed-eoa-transaction to the raw transaction hash; prepaid binds relayr-prepaid-erc2771 to its preparation ID; EntryPoint execution binds eip4337-user-operation to its immutable UserOperation record. Competing transports cannot reserve the same step, even with different wallet nonces. Failure, expiry, reorg, and retry do not release this reservation. The memory stores share one MemoryTransportReservations instance; the PostgreSQL stores share the durable table. Selecting a different transport after admission requires a new reviewed plan.
Prepaid and UserOperation execution are stored as steps[].externalExecution, without an invented owner EOA attempt, sender nonce, or signed transaction. Public responses expose steps[].execution with the transport, binding ID, chain, and any observed outer transaction hash. syncExternalExecutions discovers admitted durable reservations after a crash, including when the original plan had not yet been tagged. Reservation discovery and observation updates do not authorize a new broadcast.
getPlan and refresh obtain fresh external observations through the installed ExternalExecutionObserver. Its prepaid implementation must prove the immutable plan/record binding, exact quoted outer call, and successful expected inner Forwarder execution. The transaction service independently fetches the canonical outer receipt, checks its hash and block against that proof, applies confirmation policy, and evaluates installed semantic predicates against the full receipt logs. A successful outer receipt alone cannot confirm the step. Missing adapters, unavailable proofs, semantic failures, or reorgs block downstream calls. An outer success with a failed inner call is explicitly a failed execution. Original owner EOA nonces remain independent of the sponsor's outer transaction.
reserveExternalExecution and saveExternalExecution are internal persistence methods, never request-body admission APIs. They validate an existing permanent transport binding, immutable chain/index scope, exact initiating principal, and revision. Owners can discover and refresh their account's bot plans through read access; new financial authority is not acquired. Direct observation updates cannot mutate an external step, and direct submission cannot replace one. Recovery also discovers external-only pending journeys, while repeated plan reads recheck previously confirmed executions.
recoverPending({limit}) only reconciles pending receipts. It never broadcasts or claims work, including when the initiating grant has expired or been revoked. Owners may read their account's bot plans; bots read only their own principal's plans. New relay mutations remain bound to the plan's original principal.
Recovery uses ascending keyset pagination and returns nextCursor. An explicit cursor can resume a worker sweep; a service instance rotates its own cursor when callers omit one. Thus permanently pending older plans do not starve newer journeys. A restart can repeat harmless receipt reads.
HTTP preparation handlers call findPlanByIdempotency(actor, key, requestHash, 'create-plan') before repeating upstream work. The idempotency digest binds method, exact raw path/query, content type and exact request body bytes, so a key cannot silently identify another preparation endpoint or reserialized body.
Semantic verification receives complete bounded receipt logs before persistence compacts them. Stored receipts explicitly set logsStored: false, preserve logCount, and commit the full list as logsHash = keccak256(UTF8(canonicalJson(logs))). Canonical JSON sorts object keys lexicographically, preserves array order, uses JSON string/finite-number/boolean/null encoding, and excludes undefined object properties. Empty stored logs therefore means omitted payload, not an assertion that no events occurred. Each refresh obtains and verifies the full canonical receipt again.
Journeys and partial completion
Plans contain 1–32 calls. Dependencies refer only to distinct earlier calls, yielding a bounded DAG. A dependent call requires canonical successful confirmations and verified or explicitly unmodeled semantic status for every predecessor. unknown or failed modeled semantics block descendants. Current state is refreshed before preflight, and the store rechecks dependencies and revision during claim.
submitBundle processes explicitly supplied signed transactions in order and stops on the first unavailable or blocked step. Its response distinguishes partial admission from confirmations. It cannot make a sequence or a multi-chain journey atomic. A submitted predecessor normally leaves its dependent step blocked until confirmation; the caller resumes with the remaining signed steps later.
transactions_confirmed means only that the exact stored transactions have canonical successful receipts and any installed semantic predicates have passed. It does not imply destination bridge settlement or complete unmodeled business effects. A reorg invalidates the affected receipt and blocks unsubmitted descendants. Transactions already sent on another chain cannot be recalled. Expired plans require new plans and signatures for future calls; unknown future calldata is never appended to an existing commitment.
Smart accounts, sponsorship, and session permissions
The implemented UserOperationService prepares and publishes EntryPoint v0.7 operations for the pinned Safe7579 stack. It independently verifies account implementation/history, EntryPoint/version/chain, nonce, exact execution calldata, gas and paymaster policy, external signatures, and operation-scoped receipt evidence. Its HTTP routes are POST /user-operations, POST /user-operations/:id/submissions, and GET /user-operations/:id. Create the source plan through POST /smart-accounts/bindings/:id/plans.
Omit sessionId for fresh, finite-validity SafeOp signatures from the current Safe-owner threshold over up to sixteen ordered calls. Supply an exact active sessionId for a bound bot to sign one approved action with the installed key's EIP-191 signature. API signing remains separate in both cases. Operations outside the session's exact targets, recipients, assets, amounts and validity need a new owner-approved transaction; the API grant cannot expand that policy.
Seven/thirty-day sessions implement cumulative action, gas and payment limits with durable canonical lifecycle evidence. Funds must already be in the smart account or be deposited through a separately approved transaction; an allocation record does not fund or escrow them. Funding and allowance increases require fresh owner approval. See sessions for the supported closed actions and whole-wallet reservation/retirement rules.
Hosted owner execution requires the configured verified stack, bundler, gas-only paymaster, provider billing and sponsorship allowance. Session execution additionally requires the reviewed guard matching that paymaster and owner-approved activation. The legacy paymaster/legacy-v1 guard remain the defaults; the explicit current Pimlico profile uses current-v2 on Ethereum, Optimism, Base and Arbitrum with the same EntryPoint v0.7. Both guard artifacts are undeployed. Missing session configuration does not disable separately configured owner execution. Follow execution operations; local tests do not create provider credentials, establish billing or activate a production wallet.
UserOperation publication is attempted once after durable nonce and transport reservation. A provider timeout becomes submission_unknown; it does not inherit the direct EOA relay's explicit retry/lease behavior. Reconcile the existing operation, scoped inner execution and original plan without automatically publishing again. Safe owner rotation, API unlinking and grant revocation block applicable Center admission but do not remove a legacy onchain session: new owners should explicitly remove old permissions and revoke their enable signatures.
Verification
test/rest-transactions.test.ts exercises local signature recovery, exact calldata/value/chain checks, fee bounds, nonce readiness, unsupported envelopes, response-loss recovery, duplicate dispatch, receipt reorgs, semantic dependency blocking, bundle partial completion, and expiry/revocation. No test contacts a live chain. Store tests cover actor isolation, idempotency, concurrent nonce claims, leases, and shared revocation locks; PostgreSQL integration tests use a dedicated disposable database through TEST_DATABASE_URL.
Application-specific sponsorship
Center retains its default per-chain paymaster policy. An operator can add REST_ERC4337_SPONSOR_ROUTES, a JSON array of {id, chainId, policyId, authorizationKey}. Each route reuses that chain's reviewed provider and private Pimlico endpoints; provider keys never go to the application. The authorization key is a separate 32-byte lowercase hex secret shared with the trusted application backend. Do not expose it to browsers.
An owner may supply sponsorAuthorization on POST /api/v1/user-operations. This is a base64url-encoded JSON payload followed by a dot and the lowercase hex HMAC-SHA256 of the exact encoded payload. The payload has exactly routeId, accountId, planId, chainId, stepIndexes, idempotencyKey, issuedAt, and expiresAt. Timestamps are Unix seconds, with a maximum 300-second lifetime. The owner, immutable plan, ordered steps and request idempotency key must all match. The trusted issuer must validate the invoice and exact Center plan and reserve its sponsorship budget before issuing a voucher. A client-selected policy ID or browser Origin is not authorization.
Application sponsorship requires fresh owner signing and cannot be combined with a session. Selection persists through the operation's provider identity for submission and receipt recovery. Removing a route or changing its policy invalidates old preparations; ambiguous submissions are not retried. Requests without a voucher keep the default policy. Existing request signatures, account binding, nonce reservations, gas checks and canonical receipt verification remain required.
This mechanism selects a policy; it does not fund account creation or enforce an application's USD budget by itself. Configure provider limits and application-side durable reservations before enabling a route. Beep's initial route uses Base policy sp_soft_trish_tilby; do not replace the shared sp_wide_mastermind policy.