Bridge docs
MeterCall's Bridge is a bidirectional gateway. Smart contracts call real-world APIs. Real-world webhooks turn into on-chain attestations. Every crossing is signed with EIP-712 and verifiable by any EVM contract.
Overview
The bridge has two complementary flows:
- Chain → Real-life. A contract emits
BridgeCall(requestId, apiSlug, params, callback). The bridge listener picks it up, routes to the right HTTP API, and callsfulfill(requestId, result, sig)on the callback contract. - Real-life → Chain. A webhook arrives at
POST /v1/bridge/webhook/:slug. The bridge verifies it, produces an EIP-712 attestation signed by the hot wallet, and exposes it on/v1/bridge/verify/:requestId. Your contract consumes it viaBridgeVerifier.
# quickstart (off-chain)
curl -sX POST https://metercall.ai/v1/bridge/call \
-H 'content-type: application/json' \
-d '{"chain":"base","apiSlug":"stripe-replacement","params":{"action":"refund","amount":5000,"customer":"cus_abc"}}'
Architecture
Off-chain components
- Listener — subscribes to
BridgeCallacross 30+ chains. - Router — maps
apiSlug→ HTTP connector. - Signer — hot wallet, signs EIP-712 attestations.
- Store — immutable request/result/signature log.
On-chain components
BridgeVerifier.sol— verifies EIP-712 signatures.BridgeCallRouter.sol— example contract that emits + consumes.- Deployed on Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, Solana (via Wormhole adapter), + 22 more.
Attestation format (EIP-712)
Every bridge result is wrapped in this typed structured-data envelope:
BridgeAttestation( bytes32 requestId, string apiSlug, bytes32 resultHash, uint256 timestamp, uint256 chainId )
The domain separator uses:
Example payload returned by /v1/bridge/attest:
{
"eip712": {
"domain": { "name": "MeterCallBridge", "version": "1", "chainId": 8453 },
"types": {
"BridgeAttestation": [
{ "name": "requestId", "type": "bytes32" },
{ "name": "apiSlug", "type": "string" },
{ "name": "resultHash", "type": "bytes32" },
{ "name": "timestamp", "type": "uint256" },
{ "name": "chainId", "type": "uint256" }
]
},
"primaryType": "BridgeAttestation",
"message": {
"requestId": "0x…",
"apiSlug": "stripe-replacement",
"resultHash": "0x…",
"timestamp": 1745001234,
"chainId": 8453
}
},
"signature": "0x…65 bytes…"
}
Verifier contract (Solidity sketch)
Ship BridgeVerifier.sol and point it at MeterCall's signer address:
function verifyAttestation(
bytes32 requestId,
string memory apiSlug,
bytes32 resultHash,
uint256 timestamp,
bytes memory sig
) public view returns (bool) {
bytes32 structHash = keccak256(abi.encode(
TYPEHASH, requestId, keccak256(bytes(apiSlug)),
resultHash, timestamp, block.chainid
));
bytes32 digest = _hashTypedDataV4(structHash);
return ECDSA.recover(digest, sig) == signer;
}
See the full contract with replay protection and nonce handling in /contracts/BridgeVerifier.sol.
Chain → Real-life flow
- Your contract calls
router.request(apiSlug, params)→ emitsBridgeCall. - MeterCall listener picks up the event within ~2s.
- Bridge calls the real-world API (Stripe, Twilio, anything from the catalog).
- Bridge signs the result EIP-712, calls
router.fulfill(requestId, result, sig). - Your contract verifies the signature via
BridgeVerifierand uses the result.
// on-chain bytes32 id = router.request("stripe-replacement", abi.encode("refund", 5000, "cus_abc")); // ~2s later your fulfill() is called function fulfill(bytes32 rid, bytes memory result, bytes memory sig) external onlyBridge { require(verifier.verifyAttestation(rid, "stripe-replacement", keccak256(result), block.timestamp, sig), "bad attestation"); // decode result, credit user, whatever }
Real-life → Chain flow
- You register a webhook URL with a real-world provider pointing at
POST /v1/bridge/webhook/:slug. - Provider fires an event (charge succeeded, order placed, SMS delivered).
- Bridge verifies provider HMAC, normalizes the payload, produces an EIP-712 attestation.
- Bridge optionally posts attestation on-chain via
BridgeCallRouter.attest(), or you pull it viaGET /v1/bridge/verify/:requestIdand submit yourself. - Your contract verifies with
BridgeVerifier.
# Stripe → MeterCall POST /v1/bridge/webhook/stripe-replacement HTTP/1.1 X-MeterCall-Signature: t=1745001234,v1=... { "type":"charge.succeeded", "data":{ "amount":5000 } } # MeterCall responds with the signed attestation { "attestation_id":"0x…", "eip712":{…}, "signature":"0x…" }
Security model
- Signer isolation. The bridge hot wallet lives in an HSM-backed signer. Rotated every 30 days.
- Nonce protection.
BridgeVerifier.consumeAttestationtracks usedrequestIds; replay is impossible. - Timestamp freshness. Attestations older than 30 minutes are rejected by default (configurable per contract).
- Per-slug rate limits. Abuse on a connector is isolated; the whole bridge doesn't degrade.
- Audit log. Every bridge call is queryable at /attestations.
Supported chains
Ethereum, Base, Arbitrum, Optimism, Polygon, BSC, Avalanche, zkSync Era, Linea, Scroll, Mantle, Blast, Mode, Gnosis, Celo, Fantom, Moonbeam, Kava, Metis, Boba, Solana (Wormhole adapter), Sui (adapter), Aptos (adapter), NEAR (adapter), Stellar, Cosmos Hub, Osmosis, Injective, Sei, Polkadot.
Live chain status at /l4.
Supported APIs
Every module in the connectors catalog is bridge-ready. Today: 2,866 slugs. Target: 20M. Search, filter, click any card for the EIP-712 snippet.
Rate limits
Pricing
Pay per bridge call in PCP (MeterCall credits) or USDC. Chain → Real-life costs more than Real-life → Chain because it includes the downstream API call.
Errors
apiSlug. Check connectors.requestId doesn't exist or was evicted (500-entry ring buffer).Retry-After.