Skip to main content

Troubleshooting

Subscription issues

My subscriber signed up but their subscription shows status PENDING

The subscription exists on-chain (a SubscriptionCreated event was emitted) but the first payment hasn't executed yet. The amser keeper network executes the first charge shortly after subscription creation — typically within seconds on testnet, up to a few minutes on mainnet. If it stays PENDING longer than expected, the issue is almost always on the subscriber's side:

  • Does the subscriber have sufficient token balance (e.g. USDC) to cover the plan price?
  • Is the Permit2 allowance set correctly (amount >= plan price, expiry in the future)?

If both are correct and the subscription remains PENDING, check the activity log for a FAIL entry with an InsufficientAllowance or TransferFailed reason — the keeper attempted the charge but it failed silently from the subscriber's perspective.

Subscription shows EXPIRED but the user thinks they're subscribed

EXPIRED means now > lastPaidAt + interval + gracePeriod. This happens when a payment failed and the grace period has also lapsed. The user still has an allocation — it just isn't active. They need to ensure they have sufficient balance and an active Permit2 allowance, then the next keeper execution will renew it. If the payment window has also been missed (now > nextChargeAt + interval), the merchant or subscriber needs to call recoverSubscription(subId) on the SubscriptionModule to reset the charge window before the keeper can execute.

Subscription shows ACTIVE in the API but isActive() returns false on-chain

The indexer is eventually consistent — there may be a lag of a few seconds between a state change on-chain and the indexed state updating. Use GET /v0/auth/check?mode=onchain for authoritative results when this matters. See the auth check documentation for details on modes.

subscribeWithPermit reverted with SubscriptionAlreadyExistsForPlan

A subscriber can only have one active subscription per plan per module. If they've previously subscribed and cancelled, the allocation still exists. To re-subscribe, the user should call updateRemainingExecutions() and updateAllowanceExpiry() on the existing subscription rather than creating a new one.


Payment failures

When a keeper attempts to execute a payment and it fails, the PaymentProcessor emits an ExecutionFailed event with a FailCode. These appear as type: "FAIL" entries in the activity log with the failure reason in the reason field.

InsufficientAllowance

The Permit2 allowance is lower than the plan price. The subscriber needs to sign a new Permit2 authorization with a higher amount. The existing subscription is not cancelled — once the allowance is updated, the keeper will execute on the next cycle.

AllowanceExpired

The Permit2 allowance expiry timestamp has passed. The subscriber needs to sign a new Permit2 allowance with a future expiry. Call updateAllowanceExpiry(subId, newExpiry) on the SubscriptionModule after signing the new allowance.

TransferFailed

The token transfer reverted, typically because the subscriber's wallet balance is insufficient. No action is needed from the merchant. The keeper will retry on the next cycle. The subscription lapses after the grace period if balance is not restored.

PaymentWindowViolation

The keeper missed the payment window entirely — the current time is beyond nextChargeAt + billingInterval. This is a liveness issue, not a user error. Call recoverSubscription(subId) on the SubscriptionModule to reset the charge window, then the keeper can execute on the next cycle.

PaymentAlreadyProcessed

The idempotency bitmap already has this billing window marked as processed. This is not an error — it means the payment was already successful in a previous execution. Safe to ignore.

ProcessorPaused

The global PaymentProcessor is paused, typically during a protocol upgrade. No action is needed from merchants or subscribers. Payments will resume automatically when the processor is unpaused.

UnsupportedToken

The token used in the plan is not enabled in the TokenRegistry. This should not occur for plans created after the token was added. If it does, the token may have been disabled after plan creation. Contact the amser team.


API & integration issues

You're trying to use a session-only endpoint (e.g. create webhook, create API key) without a SIWE session. Either authenticate via SIWE first, or use an API key if the endpoint supports it. Note that API keys only work for GET requests.

403 API keys are read-only when calling the API

API keys cannot be used for POST, DELETE, or any write operation. Write operations — creating webhooks, managing API keys, modifying settings — require a SIWE session through the dashboard.

The webhook signature verification is failing

warning

Ensure you are verifying against the raw request body as a string, not a parsed JSON object. Re-serializing JSON will produce different byte sequences if key ordering or whitespace differs. Also verify that you're comparing with crypto.timingSafeEqual rather than === — timing-safe comparison is required to prevent timing attacks.

See the Signature Verification page for complete implementation examples in Node.js and Python.

My webhook endpoint is receiving duplicate events

Each event has a stable id field (e.g. evt_01j...). Implement idempotency using this ID — store processed event IDs and skip duplicates. amser retries deliveries that don't receive a 2xx response within 30 seconds, so a slow endpoint that eventually returns 200 after a timeout may receive the event again on retry. See Retry Behaviour for the full retry schedule.

GET /v0/auth/check returns authorized: false but the user subscribed successfully

Check the mode parameter. The default indexed mode reflects the indexer's current state, which may lag seconds behind the chain. Try mode=onchain for an authoritative answer. If onchain also returns false, check whether times_executed > 0 in the indexed details — a subscription that hasn't had its first payment executed yet is not considered active by isActive().

Plan price is returned as a very large number

price is returned in token base units as a string. For USDC (6 decimals), a price of "10000000" is 10 USDC. Divide by 10 ** token_decimals to get the human-readable amount. Use BigInt for the arithmetic to avoid floating-point precision loss.

const humanPrice = Number(BigInt(plan.price)) / 10 ** plan.token_decimals;